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这段代码的链路是:
- 接收 POST 请求,从 body 里取
message - 构造 messages 数组(system prompt + 用户消息)
- 调用 LLM,拿到一次性回答
- 把回答返回给前端
整个过程里没有工具,没有外部数据源,没有中间状态。模型能给出的回答,完全取决于它的训练数据。
如果用户问「杭州天气怎么样」,模型只能根据训练数据里的信息回答,它不知道 2026 年 6 月 21 日杭州的实际天气。它可能会说「杭州夏天通常很热」,但这不是实时数据,是统计印象。
这就是 Chatbot 的边界:它只能处理「模型参数里已有足够信息」的问题。
2. Chatbot的三个局限
把上面那个例子拆开看,可以归纳出三个比较稳定的局限。
局限一:不能调用外部工具
模型只能基于自身参数生成文本。它不能查数据库、不能调第三方 API、不能读写文件、不能执行 SQL。所有能力都锁在模型权重里。
用户问「我的订单 #10086 到哪了」,Chatbot 没法去订单系统查——它没有访问订单数据库的工具。
局限二:没有状态管理
每次请求独立。Chatbot 不会主动记住上一轮对话的内容,也不会把中间结果存起来供下一步使用。
虽然可以通过把历史消息塞进 messages 数组来模拟「记忆」,但这只是把上下文传给了模型,模型本身不会主动管理这些记忆。一旦消息数组截断,之前的信息就丢了。
局限三:无法自主规划
面对复杂任务,Chatbot 试图在一次回答里解决所有问题。它不会把「比较杭州和上海未来三天天气,推荐一个更适合出游的城市」拆成:
- 查杭州天气
- 查上海天气
- 比较两者
- 给出推荐
它只能一次性生成一段文字,凭训练数据里的印象来回答。这种回答在需要准确数据的场景下基本不可用。
把这三个局限放一起看:
| 局限 | 表现 | 根源 |
|---|---|---|
| 无工具 | 不能查数据库、调 API、读文件 | 模型只能生成文本,不能执行动作 |
| 无状态 | 每次请求独立,不保存中间结果 | 没有状态管理层,上下文靠外部传入 |
| 无规划 | 试图一次回答解决所有问题 | 没有循环机制,只调一次 LLM |
这三个局限指向同一个问题:单次 LLM 调用不够用了。
3. Agent的工作模式
Agent 在 Chatbot 基础上加了三样东西:工具定义、状态管理、决策循环。
最核心的变化是引入一个循环:
用户输入
↓
构造 messages → 调 LLM
↓
LLM 返回文本 → 直接给用户(结束)
↓
LLM 返回工具调用 → 执行工具 → 把结果加进 messages → 回到「调 LLM」
每一轮循环里,LLM 拿到完整的对话历史(包括之前工具调用的结果),然后决定:
- 我已经有足够信息了 → 返回文本回答,循环结束
- 我还需要更多信息 → 调用工具,拿到结果后继续下一轮
举一个具体例子。用户问「杭州今天天气怎么样,适合去哪里玩」:
- 第一轮:LLM 看到用户问题,决定先查天气 → 调用
get_weather(city: '杭州', date: '2026-06-21') - 工具返回:
{ temperature: 28, condition: '多云' } - 第二轮:LLM 看到天气结果,决定查推荐 → 调用
get_travel_recommendation(temperature: 28, condition: '多云') - 工具返回:
{ suggestion: '西湖骑行,断桥散步,建议带遮阳伞' } - 第三轮: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 能基于中间结果做下一步判断。
循环终止条件
循环什么时候结束?两个条件:
- LLM 返回的响应里没有
tool_calls→ 模型认为已经收集到足够信息,直接用文本回答 - 达到
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) })
}
}
})代码层面的区别可以总结成下面这张表:
| 维度 | Chatbot | Agent |
|---|---|---|
| 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