人人都在聊 Agent,但 90% 开发者分不清 LLM、Tool、messages 关系

最近很多人都在聊 Agent。

Cursor 是 Agent。

Claude Code 是 Agent。

Codex 是 Agent。

很多企业里的 AI 自动化产品,本质也都是 Agent。

但新手一上来很容易懵:

LLM、Agent、Tools、reasoning、messages 到底是什么关系?

为什么大模型明明会回答问题,却不能自己查股价、读文件、操作浏览器?

为什么接了 Tools 之后,AI 就像"能干活"了一样?

这篇文章用一个可运行的 Node.js 小 Demo,把这些概念讲清楚。

你能学到:

  • Agent 和 LLM 的区别
  • Tools 为什么是 Agent 的行动能力
  • messages 多轮对话怎么传
  • reasoning_content 有什么用
  • 如何用 OpenAI SDK + DeepSeek 做一次 tool calling 闭环
    代码都可以直接复制。

一、先说结论:Agent = 大脑 + 工具 + 上下文

一句话理解 Agent:

Agent 不是只会聊天的 AI,而是能理解任务、调用工具、拿到结果、继续完成任务的系统。

一个 Agent 有多强,主要看三件事:

  • 用了什么大脑:LLM
  • 装了什么工具:Tools
  • 拿到了什么信息:Context
    可以简单理解成:
txt 复制代码
Agent = LLM + Tools + Context

LLM 负责思考和生成。

Tools 负责连接外部世界。

Context 负责告诉模型现在发生了什么、之前聊了什么、可用信息有哪些。

所以 Agent 不是一个神秘概念。

它更像一个工程组合。

二、LLM:Agent 的大脑,但它本身不会行动

LLM 是大模型。

比如 DeepSeek、GPT、Claude、豆包背后的模型。

它最擅长两件事:

推理。

生成。

比如你问:

txt 复制代码
C 罗是哪个国家的足球运动员?

模型可以回答:

txt 复制代码
C 罗是葡萄牙的足球运动员。

但如果你问:

txt 复制代码
青岛啤酒今天的收盘价是多少?

问题就变了。

模型如果没有联网、没有金融接口、没有数据库,它其实并不知道实时数据。

它最多只能猜。

这就是 LLM 的边界:

它能推理和生成,但不能天然操作外部世界。

想让它真的干活,就需要 Tools。

三、Tools:让 AI 从"会说"变成"会做"

Tools 可以理解成你给模型安装的能力。

比如:

  • 查询股票价格
  • 搜索网页
  • 读取文件
  • 写入代码
  • 操作浏览器
  • 调用数据库
  • 发送消息
  • 生成图片
    没有 Tools,AI 只能回答。
    有了 Tools,AI 可以决定:
    这个问题我不能靠脑补,需要调用某个函数。
    比如用户问:
txt 复制代码
青岛啤酒的收盘价是多少?

模型应该推理出:

txt 复制代码
我需要调用 get_closing_price 工具,参数是:青岛啤酒。

然后程序真正执行这个函数:

js 复制代码
get_closing_price('青岛啤酒')

拿到结果:

txt 复制代码
67.92

再把结果交回模型。

最后模型组织成自然语言:

txt 复制代码
青岛啤酒的收盘价是 67.92。

这就是一次典型的 tool calling。

四、项目准备:一个最小 Node.js Demo

先准备一个目录:

txt 复制代码
reason-demo
├─ client.mjs
├─ completion.mjs
├─ main.mjs
├─ index.mjs
├─ package.json
└─ .env

安装依赖:

bash 复制代码
npm install openai dotenv

package.json 里依赖大概是这样:

json 复制代码
{
  "dependencies": {
    "dotenv": "^17.4.2",
    "openai": "^6.43.0"
  }
}

再准备 .env

注意不要把真实 key 提交到 Git 仓库。

bash 复制代码
DEEPSEEK_API_KEY=你的_API_KEY
DEEPSEEK_API_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-v4-flash

五、封装 client:所有请求都从这里走

先创建 client.mjs

js 复制代码
import { OpenAI } from 'openai'
import dotenv from 'dotenv'

dotenv.config()

const client = new OpenAI({
  apiKey: process.env.DEEPSEEK_API_KEY,
  baseURL: process.env.DEEPSEEK_API_BASE_URL
})

export default client

这里有两个关键点。

第一,使用 dotenv.config() 读取 .env

第二,通过 baseURL 把 OpenAI SDK 接到 DeepSeek API。

也就是说:

我们用的是 OpenAI SDK 的调用方式,但请求发到 DeepSeek 的接口地址。

六、messages:多轮对话不是字符串拼接

大模型的对话不是简单传一个字符串。

它传的是一个 messages 数组。

每一条消息都有角色。

常见角色有:

  • system:系统设定
  • user:用户输入
  • assistant:模型回复
  • tool:工具执行结果
    比如这个例子:
js 复制代码
import client from './client.mjs'

const main = async () => {
  const result = await client.chat.completions.create({
    model: 'deepseek-v4-pro',
    reasoning_effort: 'high',
    messages: [
      {
        role: 'system',
        content: '你是一个足球领域的专家,请尽量帮我回答与足球相关的问题'
      },
      {
        role: 'user',
        content: 'C 罗是哪个国家的足球运动员?'
      },
      {
        role: 'assistant',
        content: 'C 罗是葡萄牙的足球运动员'
      },
      {
        role: 'user',
        content: '内马尔呢?'
      }
    ]
  })

  console.log('思考过程:')
  console.log(result.choices[0].message.reasoning_content)

  console.log('\n最终答案:')
  console.log(result.choices[0].message.content)
}

main()

这里的关键是最后一句:

txt 复制代码
内马尔呢?

如果只看这一句,它是不完整的。

但因为前面 messages 里已经有了 C 罗的问题,所以模型能理解:

用户是在继续问:内马尔是哪个国家的足球运动员?

这就是多轮对话的核心。

不是自己拼接一大段文本。

而是维护好 messages。

七、reasoning_content:观察模型怎么想

有些模型会返回推理过程字段。

比如:

js 复制代码
result.choices[0].message.reasoning_content

它的价值不是给用户看。

而是给开发者调试。

你可以用它观察:

  • 模型有没有理解问题
  • 模型为什么要调用某个工具
  • 模型是否被 system prompt 带偏
  • 模型对上下文的判断是否正确
    比如:
js 复制代码
console.log('思考过程:')
console.log(result.choices[0].message.reasoning_content)

console.log('\n最终答案:')
console.log(result.choices[0].message.content)

这在开发 Agent 时非常有用。

因为 Agent 出错时,常见问题不是代码直接报错。

而是模型判断错了。

它可能该调工具却没调。

也可能选错工具。

还可能参数抽取错。

reasoning 能帮你更快定位问题。

八、先封装一个普通 completion 方法

如果只是普通问答,可以封装成这样。

创建 completion.mjs

js 复制代码
import client from './client.mjs'

export async function getCompletion(prompt) {
  const response = await client.chat.completions.create({
    model: process.env.DEEPSEEK_MODEL,
    messages: [
      {
        role: 'user',
        content: prompt
      }
    ]
  })

  return response.choices[0].message.content
}

使用:

js 复制代码
import { getCompletion } from './completion.mjs'

const answer = await getCompletion('请用一句话解释什么是 Agent')

console.log(answer)

这个方法适合普通文本生成。

但它还不是 Agent。

因为它没有工具。

下一步我们开始加 Tools。

九、实战:让 AI 查询股票收盘价

我们做一个最小实战:

用户问:

txt 复制代码
青岛啤酒的收盘价是多少?

模型不要直接瞎答。

它应该调用工具:

js 复制代码
get_closing_price

程序执行工具后,再让模型回答用户。

整体流程是:

txt 复制代码
用户提问
  ↓
LLM 判断是否需要工具
  ↓
返回 tool_calls
  ↓
Node.js 执行真实函数
  ↓
把工具结果加入 messages
  ↓
再次请求 LLM
  ↓
生成最终回答

十、定义 Tools:告诉模型你有什么能力

创建 index.mjs

先定义 tools。

js 复制代码
import client from './client.mjs'

const tools = [
  {
    type: 'function',
    function: {
      name: 'get_closing_price',
      description: '获取指定股票的收盘价',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: '股票名称,例如:青岛啤酒、贵州茅台'
          }
        },
        required: ['name']
      }
    }
  }
]

注意这里不是直接把函数传给模型。

而是告诉模型:

我有一个工具,名字叫 get_closing_price,它需要一个 name 参数。

其中最重要的是:

js 复制代码
description: '获取指定股票的收盘价'

这个描述越清晰,模型越容易准确调用工具。

参数描述也很重要:

js 复制代码
description: '股票名称,例如:青岛啤酒、贵州茅台'

因为模型需要从自然语言里提取参数。

十一、实现真正的工具函数

上面的 tools 只是"说明书"。

真正执行的函数还要我们自己写。

js 复制代码
function get_closing_price(name) {
  if (name === '青岛啤酒') return '67.92'
  if (name === '贵州茅台') return '1488.21'

  return '未找到股票'
}

实际项目里,这里可以换成:

  • 调用股票 API
  • 查询数据库
  • 请求后端服务
  • 读取本地文件
    但 Demo 阶段用一个普通函数就够了。
    重点是理解 tool calling 的闭环。

十二、完整可运行代码:实现一次 Agent 闭环

下面是完整的 index.mjs

可以直接复制运行。

js 复制代码
import client from './client.mjs'

const tools = [
  {
    type: 'function',
    function: {
      name: 'get_closing_price',
      description: '获取指定股票的收盘价',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: '股票名称,例如:青岛啤酒、贵州茅台'
          }
        },
        required: ['name']
      }
    }
  }
]

function get_closing_price(name) {
  if (name === '青岛啤酒') return '67.92'
  if (name === '贵州茅台') return '1488.21'

  return '未找到股票'
}

const toolMap = {
  get_closing_price
}

async function sendMessage(messages) {
  return await client.chat.completions.create({
    model: process.env.DEEPSEEK_MODEL || 'deepseek-v4-flash',
    messages,
    tools,
    tool_choice: 'auto'
  })
}

async function main() {
  const messages = [
    {
      role: 'user',
      content: '青岛啤酒的收盘价是多少?'
    }
  ]

  const firstResponse = await sendMessage(messages)
  const firstMessage = firstResponse.choices[0].message

  messages.push(firstMessage)

  const toolCalls = firstMessage.tool_calls || []

  for (const toolCall of toolCalls) {
    const toolName = toolCall.function.name
    const toolArgs = JSON.parse(toolCall.function.arguments)
    const toolFn = toolMap[toolName]

    if (!toolFn) {
      throw new Error(`未知工具:${toolName}`)
    }

    const toolResult = toolFn(toolArgs.name)

    messages.push({
      role: 'tool',
      tool_call_id: toolCall.id,
      content: String(toolResult)
    })
  }

  const finalResponse = await sendMessage(messages)
  const finalMessage = finalResponse.choices[0].message

  console.log(finalMessage.content)
}

main()

运行:

bash 复制代码
node index.mjs

你会得到类似结果:

txt 复制代码
青岛啤酒的收盘价是 67.92。

这才是完整闭环。

不是只拿到模型返回的 tool call。

而是:

模型决定调用工具,程序执行工具,模型再基于工具结果回答。

十三、拆开看:模型第一次返回了什么?

第一次请求时,messages 只有用户问题:

js 复制代码
const messages = [
  {
    role: 'user',
    content: '青岛啤酒的收盘价是多少?'
  }
]

同时告诉模型可用工具:

js 复制代码
tools,
tool_choice: 'auto'

tool_choice: 'auto' 的意思是:

让模型自己判断要不要调用工具。

如果模型认为需要工具,它会返回 tool_calls

大概长这样:

js 复制代码
{
  role: 'assistant',
  content: null,
  tool_calls: [
    {
      id: 'call_xxx',
      type: 'function',
      function: {
        name: 'get_closing_price',
        arguments: '{"name":"青岛啤酒"}'
      }
    }
  ]
}

这里要注意:

arguments 是字符串。

所以要手动解析:

js 复制代码
const toolArgs = JSON.parse(toolCall.function.arguments)

十四、工具执行结果为什么还要发回模型?

很多新手会卡在这里。

模型返回 tool call 后,为什么不直接把工具结果返回给用户?

比如工具结果是:

txt 复制代码
67.92

直接给用户看也不是不行。

但这不够自然。

更好的做法是把工具结果作为 tool 消息放回 messages:

js 复制代码
messages.push({
  role: 'tool',
  tool_call_id: toolCall.id,
  content: String(toolResult)
})

然后再请求一次模型。

模型会根据上下文生成最终回答:

txt 复制代码
青岛啤酒的收盘价是 67.92。

这一步很关键。

因为真实 Agent 里,工具结果可能很复杂。

比如:

  • 一段 JSON
  • 一组搜索结果
  • 一份文件内容
  • 一批数据库记录
    让模型再整理一次,用户体验会更好。

十五、踩坑提醒:写 Agent 最容易错的 6 个点

1. Tools 只是描述,不是函数本身

很多人以为把 tools 传给模型,模型就能直接执行函数。

不是。

模型只会返回:

txt 复制代码
我想调用哪个工具,参数是什么。

真正执行函数的是你的程序。

2. description 写得太随意

工具描述会影响模型是否准确调用。

不推荐这样写:

js 复制代码
description: '查一下'

推荐写清楚:

js 复制代码
description: '获取指定股票的收盘价'

参数也要写清楚:

js 复制代码
description: '股票名称,例如:青岛啤酒、贵州茅台'

3. 忘记把 assistant 的 tool_call 消息放回 messages

完整流程里,这一步不能省:

js 复制代码
messages.push(firstMessage)

因为后面的 tool 结果是对应这次 tool call 的。

如果少了它,模型可能无法正确理解工具结果来自哪里。

4. 忘记传 tool_call_id

工具结果消息里要带:

js 复制代码
tool_call_id: toolCall.id

它用于把工具结果和某次工具调用对应起来。

尤其当模型一次请求多个工具时,这个字段很重要。

5. arguments 忘记 JSON.parse

tool call 的参数通常是字符串:

js 复制代码
toolCall.function.arguments

要先解析:

js 复制代码
const toolArgs = JSON.parse(toolCall.function.arguments)

不要直接当对象用。

6. 把 reasoning_content 直接展示给用户

reasoning 更适合开发调试。

正式产品里不要把它直接展示给用户。

用户真正需要的是结果。

开发者才需要看模型怎么推理。

十六、Agent 和普通聊天机器人的区别

普通聊天机器人更像:

txt 复制代码
用户问 → 模型答

Agent 更像:

txt 复制代码
用户问
  ↓
模型判断任务
  ↓
选择工具
  ↓
执行工具
  ↓
观察结果
  ↓
继续推理
  ↓
返回答案或继续行动

所以 Agent 的能力边界更大。

它可以做:

  • 自动写代码
  • 自动读项目文件
  • 自动查接口文档
  • 自动生成报告
  • 自动操作网页
  • 自动调用公司内部系统
    这也是为什么现在很多 AI 产品,本质上都在往 Agent 方向发展。
    真正有价值的不是"聊得像人"。
    而是能把任务完成

十七、总结

入门 AI Agent,先抓住 4 个核心概念:

LLM 是大脑。

它负责推理和生成。

Tools 是手脚。

它负责连接外部世界,补齐模型不能直接行动的问题。

messages 是上下文。

它让模型知道之前发生了什么,当前任务进行到哪一步。

reasoning 是调试窗口。

它帮助开发者理解模型为什么这么判断。

用工程视角看,Agent 并不神秘。

它就是把模型、工具、上下文和控制流程组织起来。

这篇文章里的股票收盘价 Demo 虽然很小,但已经包含了 Agent 最关键的工作流:

模型决定调用工具 → 程序执行工具 → 工具结果回填 → 模型生成最终答案。

理解这个闭环,再去看 Cursor、Claude Code、Codex 这类产品,就会清楚很多。

我也把这次的 client.mjscompletion.mjsmain.mjsindex.mjs 拆成了完整源码,想继续对照理解的话,可以直接打开源码一步步跑(Knowledge_Repository/ai/agent/consepts at main · Guwen-yue/Knowledge_Repository)。