AI Agent:从 "对话玩具" 到 "能替你干活的同事"
2023 年 3 月,AutoGPT 的 GitHub 仓库在两周内狂揽 10 万星。所有人都在兴奋:LLM 终于不只是聊天了——它可以自己上网搜、自己写代码、自己 debug、自己完成任务。
当然,半年后大家都冷静了——AutoGPT 在大部分任务上表现不如预期,经常陷入无限循环:搜到了 → 不满意 → 重新搜 → 还是不匹配 → 换个关键词继续搜 → 永远循环。但这不重要。重要的是,Agent 这个方向被彻底打开了。
不是 "工具",是 "协作者"——Agent 的范式转移
传统的 AI 使用方式是:你输入一个问题,AI 给出一个回答。这是 "工具" 模式——就像算盘,你拨一下动一下。
Agent 模式完全不一样:你描述一个目标,Agent 自己拆解成步骤、自己搜索信息、自己调用工具、自己做决定、自己纠正错误,直到完成任务。这是 "协作者" 模式——你把任务交代给一个聪明的实习生,告诉他你想要什么,剩下的他去想办法。
这个范式转移的本质是什么?是 控制权的转移。在 "工具" 模式下,你做所有决策——问什么、怎么问、要什么格式。在 Agent 模式下,你把 "怎么做" 外包给了 AI,你只需要设定目标和约束条件。这是一步从 Copilot(副驾驶)到 Autopilot(自动驾驶)的跨越。
感知-推理-行动:一个完整的 Agent 循环
Agent 运行的底层逻辑其实非常简单——简单到你可能会说 "就这?":
观察到环境状态 → 推理(LLM 大脑)→ 选择行动 → 执行行动 → 获取新观察 → 回到第一步这个循环的本质是 闭环控制。让我用一个具体的例子来说明。
假设你告诉 Agent:"帮我订明天从北京飞上海的航班,要下午两点左右到,经济舱,价格不超过 1500。"
第 1 轮
- 观察:Agent 读取你的指令,知道自己需要查询航班
- 推理:我需要先知道明天有哪些北京飞上海的航班。我应该调用航班搜索工具。需要指定日期(2026-06-01)、出发城市(北京)、到达城市(上海)
- 行动:
search_flights(from="PEK", to="SHA", date="2026-06-01") - 新观察:返回 47 个航班结果,每个包含航班号、起降时间、价格、舱位
第 2 轮
- 观察:47 个结果太多了,需要筛选
- 推理:"下午两点左右到"——我可以设定到达时间在 13:00-15:00 之间。价格 ≤1500。经济舱。让我过滤一下
- 行动:Agent 在内部做筛选分析(不需要新工具调用)
- 推理续:筛选后剩 3 个航班:CA1502(13:30 到,¥1380)、MU5102(14:10 到,¥1420)、CZ3500(14:45 到,¥1280)
第 3 轮
- 观察:3 个候选,需要选最优的
- 推理:CZ3500 最便宜但 14:45 到有点晚。MU5102 最接近 14:00 完美但最贵。CA1502 在时间和价格之间折中。我应该把这三个列出来让用户选——他们没跟我说偏好。
- 行动:
ask_user("找到 3 个符合条件的航班,你帮我看看选哪个?[列出详情]")
你看,3 轮交互完成了任务。Agent 在这个过程中展现了:
- 工具使用:知道什么时候调用什么工具
- 约束推理:在多个约束条件(时间、价格、舱位)中做筛选
- 元认知:意识到自己不该替用户做价格 vs 时间权衡——正确地把决策权交还给用户
Function Calling:工具不是 "关键词" 是 "合约"
Function Calling 是 Agent 最底层的工具调用机制。很多人觉得它 "不就是让 LLM 输出 JSON 吗"——错。Function Calling 的核心是 LLM 理解了工具的语义——它知道一个叫 search_flights 的函数需要 from 和 to 参数,这两个参数应该是城市的 IATA 代码而不是全称,date 应该是 ISO 日期格式。
你把下面的 JSON Schema 发给 LLM 后,它可以自主决定 "为了完成用户的任务,我此刻应该调用这个函数吗?参数该怎么填?":
{
"name": "search_flights",
"description": "搜索指定日期、出发地和到达地之间的可用航班,返回航班号、起降时间、价格、舱位和余票信息",
"parameters": {
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "出发城市机场的 IATA 三字代码,如 PEK(北京首都)、SHA(上海虹桥)"
},
"to": {
"type": "string",
"description": "到达城市机场的 IATA 三字代码"
},
"date": {
"type": "string",
"description": "出发日期,格式为 YYYY-MM-DD"
},
"passengers": {
"type": "integer",
"default": 1,
"description": "乘客数量"
}
},
"required": ["from", "to", "date"]
}
}Function Calling 工程中最重要的三个原则(我自己踩坑无数后总结的):
- description 是你跟 LLM 唯一的沟通通道——花 80% 的时间打磨工具描述,比写 100 行代码有用。描述里要写明什么场景用、什么场景不用、参数的边界条件、可能遇到的错误
- Schema 验证必须严格——永远不要信任 LLM 输出的参数合法性。它完全可能传一个
from="Beijing"而不是"PEK"。框架层要做好 schema 校验 + 自动重试 - 错误处理要优雅地回传给 LLM——工具调用失败了不要直接炸掉,把错误信息格式化后送回对话流,让 LLM 自己决定 "哦,这个航班号不存在,那我改搜另一个"
ReAct:让 LLM "写出" 自己的思考
ReAct(Reasoning + Acting,Yao et al., 2022)是目前最成熟、用得最普遍的 Agent 模式。它的核心约定非常简单:LLM 在每次输出时,先输出 Thought: 表示推理,再输出 Action: 表示要调用的工具,最后处理返回的 Observation:。
一个经典的多跳问答的 ReAct 追踪:
用户:哪位美国总统在任期内获得诺贝尔和平奖的年份最晚?
Thought: 我需要找出所有获得诺贝尔和平奖的美国总统,以及他们获奖的年份,然后找最晚的。
Action: search(query="哪些美国总统获得了诺贝尔和平奖")
Observation: 西奥多·罗斯福(1906)、伍德罗·威尔逊(1919)、吉米·卡特(2002)、巴拉克·奥巴马(2009)
Thought: 四位总统获奖。卡特是2002年,奥巴马是2009年——但卡特获奖时已离任。用户问的是"在任期内获得"。让我确认卡特和奥巴马获奖时是否在任。
Action: search(query="吉米·卡特 诺贝尔和平奖 在任期内")
Observation: 吉米·卡特1977-1981年任总统,2002年获诺贝尔和平奖——获奖时已离任,不符合。
Action: search(query="巴拉克·奥巴马 诺贝尔和平奖 当选当年")
Observation: 奥巴马2009年就任总统,同年获得诺贝尔和平奖——在任期内。
Thought: 奥巴马是在任期内获得诺贝尔和平奖的美国总统中获奖年份最晚的(2009年)。罗斯福1906年、威尔逊1919年都在任期内,但年份更早。
Final Answer: 巴拉克·奥巴马(Barack Obama),在2009年就任总统同年获得诺贝尔和平奖。注意这段追踪里发生的事情:Agent 检索到卡特后,自己意识到 "等等,他获奖时不在任上"——这是 自我修正(self-correction),ReAct 最核心的价值。因为思考过程被显式写出来了,模型有机会在后续步骤中检查前面的假设。而传统的 "模型直接输出答案" 模式,第一反应如果错了就错了,没有挽回机会。
ReAct 的局限性也很明显:推理成本高(每次思考都要生成 token),容易写偏(误入歧途后很难跳出来),对超长任务支持不好(上下文越长,前面思考越容易失效)。
多 Agent 系统:"让三个半瓶醋干一个全瓶醋的活"
这是一个违反直觉的发现:3 个 7B 模型协作,居然能在某些复杂任务上超过一个 70B 模型单独干。 为什么?
答案是角色分化。一个人类团队里,你让一个人同时做设计、编码、测试、沟通——他很厉害,但会累、会遗漏、会钻牛角尖。但如果分成:一个写代码、一个 review 代码、一个写测试——三个人互相检查、互相补充,犯错的概率大幅下降。
经典的多 Agent 架构:
┌─────────┐
│ Manager │ ← 拆解任务、分配子任务、汇总结果
└────┬────┘
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Coder │ │ Reviewer │ │ Tester │
│ 写代码 │ │ 审查代码 │ │ 写测试 │
└──────────┘ └──────────┘ └──────────┘
│ │ │
└────────────┼────────────┘
▼
┌──────────────┐
│ Synthesizer │ ← 整合结果、处理冲突
└──────────────┘在工作中实际用过多 Agent 的人都会告诉你同一个感受:debug 太难了。 因为信息在 Agent 之间传递时,每一跳都可能引入误差和歧义。一个 Agent 的输出变成另一个 Agent 的输入——如果中间某个 Agent 产生了幻觉或不完整信息,后面的 Agent 就会基于错误的前提工作。这是多 Agent 系统现阶段最大的工程挑战。
目前最成熟的框架是微软的 AutoGen(2023)和 CrewAI。AutoGen 的好处是对话驱动——Agent 之间的沟通就是 LLM 对话,调试时可以完整回溯全部对话历史,定位问题。
LangGraph vs LangChain:状态机才是正确的抽象
LangChain(2022)最早尝试把 Agent 抽象成 "Chain"——一系列按顺序执行的步骤。但问题是 Agent 的执行不是线性的——它充满了条件分支、循环、错误路径和回溯。你没法用一个 Chain 来描述 "如果搜索失败则修改 query 重新搜索,最多重试 3 次,3 次后如果还没结果就告诉用户"。
LangGraph(2024)把抽象改成了有状态图(Stateful Graph)——跟状态机一模一样。节点是操作(调用 LLM、调用工具),边是状态转移条件。
from langgraph.graph import StateGraph, END
from typing import TypedDict
class AgentState(TypedDict):
messages: list
tool_results: list
retry_count: int
workflow = StateGraph(AgentState)
# 定义节点
workflow.add_node("llm", call_model) # LLM 推理
workflow.add_node("tools", execute_tools) # 工具执行
workflow.add_node("finalize", format_output) # 整理输出
# 条件边:LLM 输出后判断
def should_use_tools(state):
last_msg = state["messages"][-1]
if last_msg.tool_calls:
return "tools" # 有工具调用,去执行
return "finalize" # 没有工具调用,直接结束
workflow.add_conditional_edges("llm", should_use_tools, {
"tools": "tools",
"finalize": "finalize"
})
workflow.add_edge("tools", "llm") # 工具结果送回 LLM
workflow.add_edge("finalize", END)
app = workflow.compile()
result = app.invoke({"messages": [HumanMessage(content="帮我查一下今天的天气")], "retry_count": 0})我个人认为 LangGraph 的状态图抽象是当前最正确的 Agent 编排范式——它干净、可测试、可可视化。每个状态转换都显式定义,不会有 LangChain 那种 "藏在 Chain 里面的魔法" 的不透明感。
Agent 的四大现实痛点
1. 无限循环:Agent 最常见的 failure mode。搜索 → 不满意 → 换关键词 → 不满意 → 再换。解决方案:硬性限制最大步数(比如 15 步),加入 "如果连续 3 步没有进展就停下来求助" 的元认知检查。
2. 成本(token 爆炸):一个 ReAct Agent 完成复杂任务可能消耗 10 万-50 万 token。按 GPT-4 的价格($5/1M 输入 token、$15/1M 输出 token),一次复杂任务可能花 $2-8。如果你的产品一天处理 1000 个任务,那就是 $2000-8000/天——一个月 $6-24 万。Agent 的成本控制是商业化的最大瓶颈。
3. 上下文窗口管理:每一次工具调用的结果都会塞进上下文——一个 10 步的 Agent 执行下来,对话历史可能几千行。模型在上下文窗口的前 20% 的表现远好于后 80%——这被称为 "Lost in the Middle" 现象。需要做上下文压缩、只保留关键信息、定期做摘要。
4. 工具可靠性:LLM 调用工具时经常出错——参数格式不对、参数值超出合理范围、选错工具。工具本身也可能超时、返回野数据、被风控拦截。在 Agent 框架里,你永远要假定工具会失败,永远要有 fallback 路径。 这不是过度设计,这是血的教训。
MCP:Agent 的 "USB-C" 时刻
如果说 Function Calling 定义了 "单个模型怎么调单个工具",那 MCP(Model Context Protocol) 定义了 "任意模型怎么发现和调用任意工具"。它是 Anthropic 在 2024 年底开源的一个开放协议,用 JSON-RPC 2.0 标准化了模型与外部工具/数据源的交互。
MCP 带来的关键变化是:以前如果你想在 Claude Desktop 里加一个 "查询数据库" 的功能,你需要专门为 Claude Desktop 写一个插件。现在你只需要写一个 MCP Server(暴露 query_database 工具),然后它就能被 Claude Desktop、VS Code、Cursor、Continue 等所有 MCP 兼容的 AI 应用使用——一次编写,到处运行。
MCP 的三类原语(Tools、Resources、Prompts)构成了一个完整的上下文感知体系:Tools 是可调用的功能(执行动作)、Resources 是可访问的数据(获取信息)、Prompts 是预定义模板(控制行为)。三者组合,构成了 Agent 的能力拼图。
目前 MCP 还是早期阶段——安全性(工具权限控制)、协议版本稳定性、多步工具链调用等问题都还在解决中。但方向是对的。Agent 的未来不在于 "更大的模型",而在于 "模型跟世界的交互变得更高效、更可靠、更安全"。