从“问答”到“行动”:Agent 是什么

普通的对话式模型只能“说”,Agent 的目标是“做”。它把大模型放进一个循环:观察当前状态 → 决定下一步动作 → 调用工具执行 → 把结果放回上下文 → 继续判断,直到任务完成。

这个循环带来两个根本变化。第一,模型获得了与外部世界交互的能力:查数据库、调接口、写文件、发消息。第二,任务的完成不再依赖一次生成,而是依赖多步决策——这既提升了上限,也引入了新的失败模式:死循环、工具误用、目标漂移。

Agent 的本质不是更聪明的模型,而是把“决策”与“执行”解耦后形成的闭环系统。

理解这一点后你会发现,构建 Agent 的难点很少在模型本身,而在循环控制、工具设计与错误处理。

ReAct:推理与行动交替的核心循环

ReAct(Reasoning + Acting)是最经典也最实用的 Agent 范式。它让模型在每一步先输出一段思考(Reasoning),再输出一个动作(Action)及其参数,系统执行动作后把观察结果(Observation)追加回上下文,模型据此继续。

这种交错结构的价值在于用外部反馈校准内部推理。模型不需要一次猜对所有事,它可以先试探、看结果、再修正。相比“一次性给答案”,多步交互能处理信息不完整、需要试错的任务。

实践中,思考部分建议限制长度(比如 100 字以内),并要求模型用固定关键字标记动作(如 Action: search、Action Input: {...})。这样系统可以用简单解析器稳定地取出动作,而不是依赖脆弱的自然语言理解。

工具调用:把函数暴露给模型

工具是 Agent 的手脚。一个工具就是一段可以被模型调用的函数:名称、用途描述、参数结构、返回值。设计工具时有三条经验。

Agent 工具设计的三个要点
  • 描述即文档模型的选型完全依赖工具描述,写清“什么时候用、什么时候不要用”。
  • 粒度适中太粗一件事做不完,太细要调十几次;一个工具对应一个完整语义动作。
  • 错误友好返回结构化错误信息(原因 + 建议),让模型有机会自我修正而不是直接崩掉。
AI 智能体示意:中央圆环机器人图标向四个工具模块发出发光连线,模块分别表示检索、计算、代码与记忆
Agent 的核心循环:一个决策中枢,加上若干能力边界清晰的工具模块。

另一个常被忽略的点是工具数量的上限。当你有几十个工具时,模型的选择准确率会明显下降。解决方案是按场景做工具子集、引入检索式的工具路由,或者把多个相关能力合并成一个带 mode 参数的工具。

工具定义的写法各平台略有差异,但结构是一致的:一个 JSON Schema 描述参数,一个本地函数负责执行。下面这段代码不依赖任何第三方库,可以直接运行,演示“模型给的参数 → 本地工具执行 → 结构化结果回填”的完整链路:

import json

# 1) 暴露给模型的工具描述(真实项目里直接提交给工具调用接口)
TOOLS = [{
    "name": "get_weather",
    "description": "查询指定城市当前天气。仅当用户明确询问天气时使用。",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "城市名,如 佛山"},
            "unit": {"type": "string", "enum": ["c", "f"], "default": "c"}
        },
        "required": ["city"]
    }
}]

# 2) 本地实现:成功返回 dict,失败返回结构化错误,便于模型自我修正
def get_weather(city, unit="c"):
    fake_db = {"佛山": 28, "北京": 22}
    if city not in fake_db:
        return {"ok": False, "error": "CITY_NOT_FOUND", "hint": "请确认城市名,或改用附近城市"}
    t = fake_db[city]
    if unit == "f":
        t = round(t * 9 / 5 + 32)
    return {"ok": True, "city": city, "temperature": t, "unit": unit}

# 3) 调度器:校验参数 -> 执行 -> 统一回填
REGISTRY = {"get_weather": get_weather}

def dispatch(tool_call):
    name = tool_call.get("name")
    args = tool_call.get("arguments", {})
    fn = REGISTRY.get(name)
    if fn is None:
        return {"ok": False, "error": "UNKNOWN_TOOL", "hint": "可用工具:%s" % list(REGISTRY)}
    try:
        return fn(**args)
    except TypeError as e:
        return {"ok": False, "error": "BAD_ARGUMENTS", "hint": str(e)}

if __name__ == "__main__":
    calls = [
        {"name": "get_weather", "arguments": {"city": "佛山"}},
        {"name": "get_weather", "arguments": {"city": "佛山", "unit": "f"}},
        {"name": "get_weather", "arguments": {"city": "火星"}},
        {"name": "get_time", "arguments": {}},
    ]
    for c in calls:
        print(c["name"], "->", json.dumps(dispatch(c), ensure_ascii=False))

把它跑起来会看到四种结果:正常返回、单位换算、城市不存在、工具不存在。最后两种正是 Agent 能否自我修正的关键——错误信息足够结构化,模型才有机会换个城市或换工具重试,而不是把异常抛给用户。

记忆:短期上下文与长期存储

记忆分两层。短期记忆就是上下文窗口:把最近几轮的对话、工具返回值按顺序保留。它的限制很明确——装不下、会变慢、越靠中间越容易被忽略。因此常见的做法是滑动窗口加摘要:超出长度的部分压缩成一段要点摘要,放在对话开头。

长期记忆需要外部存储:向量库存语义片段,关系库存结构化事实(用户偏好、项目配置、历史结论)。写入时机很关键——不是每句话都存,而是抽取稳定、可复用的信息。读取时也要控制数量,检索回来的内容要做重排序,只把最相关的几条塞进上下文。

一个实用原则:能放进工具结果的东西,不要塞进记忆。记忆应该是“跨会话的稳定知识”,而不是“这次任务的临时数据”。

规划与反思:让 Agent 少走弯路

规划让 Agent 在动手前先想清楚路径。最简单的形式是让模型先输出一个待办列表,然后逐步执行并在完成后打勾。这比让模型边走边看更稳定,因为任务被显式分解后,遗漏步骤的概率大幅下降。

反思(Reflection)是另一半。每完成一个子任务,让模型检查结果是否满足目标:数据是否完整、格式是否正确、有没有与已有信息矛盾。发现问题就让循环继续,而不是直接结束。注意反思要设置次数上限,否则容易出现“反复自我否定、永不停机”的情况。

如果任务足够复杂,可以采用“先规划、再执行、最后校验”的三段式:规划阶段只产出步骤和判据,执行阶段只做单步子任务,校验阶段对照判据逐项检查。这三段甚至可以用不同的提示词与不同的模型——用便宜模型做执行、用强模型做规划与校验,能显著压低成本。

多智能体协作:分工与通信

多智能体不是噱头,它的价值在上下文隔离。当单个 Agent 需要同时掌握大量彼此无关的信息时,上下文会被稀释,决策质量下降。把任务拆给多个各司其职的 Agent,每个只看到自己关心的部分,效果往往更好。

常见分工模式有三种:“经理 + 工人”,一个负责拆解与汇总,其余负责执行;“流水线”,各 Agent 按顺序处理同一份数据的不同阶段;“辩论”,多个 Agent 独立作答再由一个仲裁者比较。工程上最稳的是第一种,最容易失控的是第三种。

通信协议要简单:统一的消息结构(谁、做什么、结果、状态),明确的任务边界,以及一个全局的终止条件。缺少终止条件是多智能体系统失控的首要原因。