Agent与普通Chatbot区别

要点

  • 普通 Chatbot 的工作模式是一次 LLM 调用:接收输入、调用模型、返回输出,整个过程里没有外部工具参与
  • Chatbot 有三个核心局限:不能调用外部工具、没有跨请求状态管理、无法自主规划多步任务
  • Agent 在 Chatbot 基础上加了工具调用、状态管理和决策循环,能根据中间结果决定下一步做什么
  • Agent 的核心是一个 while 循环:调 LLM → 检查是否要调用工具 → 执行工具 → 把结果送回 LLM → 重复,直到模型给出最终文本回答
  • 代码实现上,Chatbot 是一次 LLM 调用加一次返回;Agent 是一个循环,循环里包含 LLM 推理、工具执行和结果回填
  • 选型原则:一次 LLM 调用能完成的任务用 Chatbot,需要查数据库、调 API、多步计算的任务用 Agent

1. Chatbot的工作模式

先看一个最常见的场景:用户在前端输入一段文字,后端把它转给大模型,拿到回答后返回。这就是 Chatbot 的标准工作模式——输入 → 输出,一次请求对应一次 LLM 调用。

用 Hono 写一个最简版本:

// src/chatbot/simple.ts
import { Hono } from 'hono'
 
type Env = {
  AI: Ai
}
 
const app = new Hono<{ Bindings: Env }>()
 
app.post('/chat', async (c) => {
  const { message } = await c.req.json<{ message: string }>()
 
  const response = await c.env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
    messages: [
      { role: 'system', content: '你是一个有用的助手。' },
      { role: 'user', content: message },
    ],
  })
 
  return c.json({ reply: response })
})
 
export default app

这段代码的链路是:

  1. 接收 POST 请求,从 body 里取 message
  2. 构造 messages 数组(system prompt + 用户消息)
  3. 调用 LLM,拿到一次性回答
  4. 把回答返回给前端

整个过程里没有工具,没有外部数据源,没有中间状态。模型能给出的回答,完全取决于它的训练数据。

如果用户问「杭州天气怎么样」,模型只能根据训练数据里的信息回答,它不知道 2026 年 6 月 21 日杭州的实际天气。它可能会说「杭州夏天通常很热」,但这不是实时数据,是统计印象。

这就是 Chatbot 的边界:它只能处理「模型参数里已有足够信息」的问题。

2. Chatbot的三个局限

把上面那个例子拆开看,可以归纳出三个比较稳定的局限。

局限一:不能调用外部工具

模型只能基于自身参数生成文本。它不能查数据库、不能调第三方 API、不能读写文件、不能执行 SQL。所有能力都锁在模型权重里。

用户问「我的订单 #10086 到哪了」,Chatbot 没法去订单系统查——它没有访问订单数据库的工具。

局限二:没有状态管理

每次请求独立。Chatbot 不会主动记住上一轮对话的内容,也不会把中间结果存起来供下一步使用。

虽然可以通过把历史消息塞进 messages 数组来模拟「记忆」,但这只是把上下文传给了模型,模型本身不会主动管理这些记忆。一旦消息数组截断,之前的信息就丢了。

局限三:无法自主规划

面对复杂任务,Chatbot 试图在一次回答里解决所有问题。它不会把「比较杭州和上海未来三天天气,推荐一个更适合出游的城市」拆成:

  1. 查杭州天气
  2. 查上海天气
  3. 比较两者
  4. 给出推荐

它只能一次性生成一段文字,凭训练数据里的印象来回答。这种回答在需要准确数据的场景下基本不可用。

把这三个局限放一起看:

局限表现根源
无工具不能查数据库、调 API、读文件模型只能生成文本,不能执行动作
无状态每次请求独立,不保存中间结果没有状态管理层,上下文靠外部传入
无规划试图一次回答解决所有问题没有循环机制,只调一次 LLM

这三个局限指向同一个问题:单次 LLM 调用不够用了。

3. Agent的工作模式

Agent 在 Chatbot 基础上加了三样东西:工具定义、状态管理、决策循环。

最核心的变化是引入一个循环:

用户输入
  ↓
构造 messages → 调 LLM
  ↓
LLM 返回文本 → 直接给用户(结束)
  ↓
LLM 返回工具调用 → 执行工具 → 把结果加进 messages → 回到「调 LLM」

每一轮循环里,LLM 拿到完整的对话历史(包括之前工具调用的结果),然后决定:

  • 我已经有足够信息了 → 返回文本回答,循环结束
  • 我还需要更多信息 → 调用工具,拿到结果后继续下一轮

举一个具体例子。用户问「杭州今天天气怎么样,适合去哪里玩」:

  1. 第一轮:LLM 看到用户问题,决定先查天气 → 调用 get_weather(city: '杭州', date: '2026-06-21')
  2. 工具返回:&#123; temperature: 28, condition: '多云' &#125;
  3. 第二轮:LLM 看到天气结果,决定查推荐 → 调用 get_travel_recommendation(temperature: 28, condition: '多云')
  4. 工具返回:&#123; suggestion: '西湖骑行,断桥散步,建议带遮阳伞' &#125;
  5. 第三轮:LLM 看到所有结果,生成最终回答 → 返回文本,循环结束

三轮调用,每轮都基于上一轮的工具结果做判断。LLM 不是一次性生成所有内容,而是分步收集信息、逐步构建回答。

4. Agent代码实现

用 TypeScript 和 Hono 写一个 Agent 核心循环。先看完整代码,再拆解关键部分。

// src/agent/index.ts
import { Hono } from 'hono'
 
type Env = {
  AI: Ai
  DB: D1Database
}
 
// 工具定义:告诉 LLM 有哪些工具可用
const toolDefinitions = [
  {
    name: 'get_weather',
    description: '查询指定城市指定日期的天气',
    parameters: {
      type: 'object' as const,
      properties: {
        city: { type: 'string', description: '城市名称' },
        date: { type: 'string', description: '日期,格式 YYYY-MM-DD' },
      },
      required: ['city', 'date'],
    },
  },
  {
    name: 'get_travel_recommendation',
    description: '根据天气情况推荐旅行目的地',
    parameters: {
      type: 'object' as const,
      properties: {
        temperature: { type: 'number', description: '温度(摄氏度)' },
        condition: { type: 'string', description: '天气状况' },
      },
      required: ['temperature', 'condition'],
    },
  },
]
 
// 工具实现:实际执行工具逻辑
async function executeTool(
  name: string,
  args: Record<string, unknown>,
  env: Env
): Promise<unknown> {
  if (name === 'get_weather') {
    const record = await env.DB
      .select()
      .from(weatherTable)
      .where(eq(weatherTable.city, args.city as string))
      .get()
    return record ?? { temperature: 25, condition: '晴' }
  }
 
  if (name === 'get_travel_recommendation') {
    const temp = args.temperature as number
    if (temp > 30) {
      return { suggestion: '推荐去海边或山区避暑,如青岛、莫干山' }
    }
    return { suggestion: '适合城市游览,推荐西湖、乌镇' }
  }
 
  return { error: `未知工具:${name}` }
}
 
// Agent 核心路由
const app = new Hono<{ Bindings: Env }>()
 
app.post('/agent', async (c) => {
  const { message } = await c.req.json<{ message: string }>()
 
  const toolMap: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
    get_weather: (args) => executeTool('get_weather', args, c.env),
    get_travel_recommendation: (args) => executeTool('get_travel_recommendation', args, c.env),
  }
 
  const messages: Array<{ role: string; content: string; tool_calls?: unknown[]; tool_call_id?: string; name?: string }> = [
    { role: 'system', content: '你是一个天气和旅行助手。需要查天气或推荐时,请使用工具。' },
    { role: 'user', content: message },
  ]
 
  const maxIterations = 10
  for (let i = 0; i < maxIterations; i++) {
    const response = await c.env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
      messages,
      tools: toolDefinitions,
    })
 
    // 没有工具调用 → 模型认为已有足够信息,返回最终回答
    if (!response.tool_calls || response.tool_calls.length === 0) {
      return c.json({ reply: response })
    }
 
    // 有工具调用 → 逐个执行,把结果加进 messages
    const assistantMessage = response as { role: string; content: string; tool_calls: Array<{ id: string; name: string; arguments: Record<string, unknown> }> }
    messages.push({
      role: 'assistant',
      content: assistantMessage.content ?? '',
      tool_calls: assistantMessage.tool_calls,
    })
 
    for (const toolCall of assistantMessage.tool_calls) {
      const fn = toolMap[toolCall.name]
      if (!fn) {
        messages.push({
          role: 'tool',
          tool_call_id: toolCall.id,
          name: toolCall.name,
          content: JSON.stringify({ error: `未知工具:${toolCall.name}` }),
        })
        continue
      }
 
      const result = await fn(toolCall.arguments)
      messages.push({
        role: 'tool',
        tool_call_id: toolCall.id,
        name: toolCall.name,
        content: JSON.stringify(result),
      })
    }
 
    // 循环继续:LLM 会看到工具调用结果,决定下一步
  }
 
  return c.json({ reply: '达到最大迭代次数,停止处理。' }, 500)
})
 
export default app

这段代码拆开来看,有三个关键部分。

工具定义和工具实现分离

toolDefinitions 数组告诉 LLM「你有哪些工具可以用,每个工具需要什么参数」。这是一份契约,LLM 根据它来决定是否调用工具、怎么填参数。

executeTool 函数是实际执行逻辑。它接收工具名和参数,查数据库或做计算,返回结果。

这两者分开是因为:LLM 只需要知道工具长什么样(名称、描述、参数 schema),不需要知道工具内部怎么实现。反过来,工具实现不需要关心 LLM 的推理过程。

messages 数组是状态容器

每轮循环里,messages 数组不断增长:

初始:[system, user]
第 1 轮工具调用后:[system, user, assistant(含 tool_calls), tool(结果)]
第 2 轮工具调用后:[system, user, assistant(含 tool_calls), tool(结果), assistant(含 tool_calls), tool(结果)]
第 3 轮无工具调用:LLM 返回最终文本,循环结束

messages 就是 Agent 的状态。每一轮循环,LLM 都能看到完整历史,包括之前所有工具调用的输入和输出。这使得 LLM 能基于中间结果做下一步判断。

循环终止条件

循环什么时候结束?两个条件:

  1. LLM 返回的响应里没有 tool_calls → 模型认为已经收集到足够信息,直接用文本回答
  2. 达到 maxIterations 上限 → 防止意外死循环

maxIterations 是安全阀。正常情况下 LLM 会在几轮之内给出回答,但异常情况(工具一直返回错误、LLM 反复调用同一个工具)可能导致循环卡住。设一个上限能避免资源浪费。

5. Chatbot与Agent对比

把两种模式的代码放一起对比。

Chatbot 的核心:

// src/chatbot/core.ts
app.post('/chat', async (c) => {
  const { message } = await c.req.json<{ message: string }>()
 
  // 一次 LLM 调用
  const response = await c.env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
    messages: [
      { role: 'system', content: '你是一个有用的助手。' },
      { role: 'user', content: message },
    ],
  })
 
  // 直接返回
  return c.json({ reply: response })
})

Agent 的核心(简化版):

// src/agent/core.ts
app.post('/agent', async (c) => {
  const { message } = await c.req.json<{ message: string }>()
 
  const messages = [
    { role: 'system', content: '你是一个天气和旅行助手。' },
    { role: 'user', content: message },
  ]
 
  // 循环调用 LLM
  for (let i = 0; i < maxIterations; i++) {
    const response = await c.env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
      messages,
      tools: toolDefinitions,
    })
 
    // 无工具调用 → 结束
    if (!response.tool_calls?.length) {
      return c.json({ reply: response })
    }
 
    // 有工具调用 → 执行并把结果加进 messages
    messages.push({ role: 'assistant', content: '', tool_calls: response.tool_calls })
    for (const tc of response.tool_calls) {
      const result = await executeTool(tc.name, tc.arguments, c.env)
      messages.push({ role: 'tool', tool_call_id: tc.id, name: tc.name, content: JSON.stringify(result) })
    }
  }
})

代码层面的区别可以总结成下面这张表:

维度ChatbotAgent
LLM 调用次数每次请求 1 次每次请求 1 ~ N 次(循环)
工具调用有,LLM 自主决定调哪些工具
状态管理无,每次请求独立有,messages 数组跨轮次积累
多步推理不支持支持,每轮基于上一轮结果决策
外部数据源不能访问通过工具访问数据库、API 等
终止条件LLM 返回即终止LLM 不调用工具 或 达到迭代上限
适用任务单轮文本转换需要外部数据或多步计算的任务
开发复杂度较高,需要定义工具、管理状态、处理异常
延迟1 次 LLM 调用延迟N 次 LLM 调用 + N-1 次工具执行延迟

6. 何时用Chatbot,何时用Agent

选型的关键在于:任务是否需要外部工具或多步推理。

适合用 Chatbot 的场景:

  • FAQ 问答:用户问一个问题,模型从已有知识里回答
  • 文本翻译:输入中文,输出英文
  • 文本摘要:输入长文,输出摘要
  • 情感分析:输入评价,输出正面/负面
  • 代码片段生成:描述需求,输出代码
  • 格式转换:自然语言转 JSON、JSON 转自然语言

这些任务的共同点是:模型参数里的信息足够回答问题,一次调用就能完成。

适合用 Agent 的场景:

  • 需要查数据库:「我的订单到哪了」「上个月的销售数据是多少」
  • 需要调外部 API:「北京今天天气如何」「当前美元汇率是多少」
  • 需要多步计算:「比较三个城市的天气,推荐最合适的」
  • 需要实时数据:模型训练数据截止之后的信息
  • 需要条件分支:根据查询结果决定走哪条处理路径

这些任务的共同点是:一次 LLM 调用不够,需要中间插入工具调用、获取外部数据、再做下一步判断。

中间地带

实际项目里还有不少场景处于两者之间。比如「查一次数据库就能回答的问题」——不需要多步推理,但需要一次工具调用。

这种情况可以做一个轻量 Agent:只定义一个工具,循环上限设为 2(一次工具调用 + 一次最终回答)。这样既保持了 Agent 的灵活性,又不会因为过度设计增加复杂度。

很多生产系统的做法是:默认走 Chatbot,在特定路径上启用 Agent 能力。简单问题用 Chatbot 快速响应,需要工具时再走 Agent 链路。这种混合架构能在开发成本和功能覆盖之间取得平衡。

总结

回顾一下这篇的要点:

  • Chatbot 是「输入 → 调 LLM → 输出」的单向流程,不能调工具、不管状态、不做多步推理
  • Agent 的核心是一个循环:调 LLM → 判断是否需要工具 → 执行工具 → 把结果送回 LLM → 重复,直到模型给出文本回答
  • 两者的代码区别在于:Chatbot 是一次 LLM 调用加一次 return;Agent 是一个 while 循环,循环里包含 LLM 推理、工具执行和状态积累
  • Agent 比 Chatbot 多了工具调用、状态管理和自主决策的能力,代价是更高的开发成本、更长的响应延迟和更复杂的异常处理
  • 选型看任务:一次 LLM 调用能完成的用 Chatbot,需要外部数据或多步推理的用 Agent