Skip to content

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 的函数需要 fromto 参数,这两个参数应该是城市的 IATA 代码而不是全称,date 应该是 ISO 日期格式。

你把下面的 JSON Schema 发给 LLM 后,它可以自主决定 "为了完成用户的任务,我此刻应该调用这个函数吗?参数该怎么填?":

json
{
  "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 工程中最重要的三个原则(我自己踩坑无数后总结的):

  1. description 是你跟 LLM 唯一的沟通通道——花 80% 的时间打磨工具描述,比写 100 行代码有用。描述里要写明什么场景用、什么场景不用、参数的边界条件、可能遇到的错误
  2. Schema 验证必须严格——永远不要信任 LLM 输出的参数合法性。它完全可能传一个 from="Beijing" 而不是 "PEK"。框架层要做好 schema 校验 + 自动重试
  3. 错误处理要优雅地回传给 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、调用工具),边是状态转移条件。

python
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 的未来不在于 "更大的模型",而在于 "模型跟世界的交互变得更高效、更可靠、更安全"。

基于 VitePress 构建 | 部署于 Cloudflare Pages