Agent 是什么:从闭环到自主决策

作者:toy


一、智能体不是聊天机器人

有一个误解在 2025 年前后反复出现:只要套上”AI Agent”的名字,就算 Agent 了。实则不然。一个每次都从零开始的对话框,无论填了多么精心的 system prompt,都不是 Agent。它只是一个有记忆加成的问答接口。

区分 Agent 和 LLM 对话的核心不是技术栈,而是一个问题:它能不能在没有人工逐步引导的情况下,完成一件有始有终的事?

打一个类比:公司来了一名新实习生。第一天,你问他任何问题,他都能给出有条理的回答——这是 LLM 对话的能力,知识面广、表达清晰、反应快。但如果你想让他独立负责一个项目,比如”把这季度的客户投诉整理成报告、找出根因、拟一份改进方案”,他就需要另一套能力:记住整个任务上下文、主动去找需要的信息、按步骤推进、遇到卡点自己想办法或来问你。这才是 Agent 的工作模式。

Agent 和 LLM 对话的差距,体现在四个地方。

首先是目标持有。Agent 拿着一个需要多步骤完成的任务,不是只响应当前这条消息。目标会跨越多轮保持,驱动后续每一个行动。

其次是行动能力。Agent 能调用工具:搜索、写文件、发 HTTP 请求、运行代码。输出从文字变成了真实世界里的操作。

第三是状态记忆。Agent 要知道自己做到哪里了、做了什么决定、遇到了什么障碍。这个状态不能随对话轮次消失。

第四是循环执行。基础模式是:感知当前环境→决策下一步行动→执行→观察结果→再决策。循环不需要人类逐步介入。

缺了其中任何一点,都只是模拟了 Agent 的形。

从工程角度看,Agent = Model + Harness。模型提供推理能力,Harness(执行框架)提供循环、工具调用、记忆管理、状态追踪这些基础设施。Anthropic 内部有一句话:”如果你不是模型,你就是 Harness。” 这个比喻很精确:LLM 相当于 CPU,它本身不带 RAM、磁盘和 IO;上下文窗口是 RAM;外部数据库是磁盘;工具是设备驱动;Harness 就是操作系统,把这一切协调起来让 CPU 能干活。


二、感知、规划、记忆:三个核心模块

Agent 的内部结构可以从三个维度来拆解:感知层负责把外部世界的信息转化为 Agent 可以处理的上下文;规划层负责根据目标决定接下来要做什么;记忆层负责让已有的信息和状态在需要时可以被召回。

感知层:世界如何进入上下文

对 Agent 来说,”感知”就是”把信息塞进上下文窗口”。这听起来简单,但实际上是 Agent 工程中最需要精心设计的部分之一。

信息来源可以是多样的:用户发来的文本、图像、文件;工具调用返回的结果;数据库查询的返回值;外部 API 的响应。所有这些,最终都需要被转化成 LLM 能理解的 token 序列。

感知层要做两个决策:塞什么,以及怎么塞。

塞什么比看起来难。上下文窗口空间有限,把所有东西都塞进去会降低模型对关键信息的注意力。研究数据显示,当信息位于上下文窗口的中间位置时,模型的注意力会下降 30% 以上。重要信息堆在中间,模型很可能视而不见。

怎么塞涉及格式和结构。工具返回的 JSON、网页抓取的 HTML、用户上传的 PDF,各有各的格式。感知层负责解析、过滤、格式化,把原始数据转成对 LLM 友好的结构化输入。

一个典型的感知层处理流程:

# 感知层:将原始输入转化为上下文片段
def perceive(raw_inputs: list[RawInput]) -> ContextFragment:
    fragments = []

    for inp in raw_inputs:
        if inp.type == "tool_result":
            # 工具返回值需要截断并格式化
            content = truncate(inp.data, max_tokens=2000)
            fragments.append(f"[工具返回] {inp.tool_name}:n{content}")

        elif inp.type == "file":
            # 文件内容可能需要分块处理
            chunks = chunk_document(inp.content)
            relevant = retrieve_relevant(chunks, query=current_goal)
            fragments.append(format_chunks(relevant))

        elif inp.type == "user_message":
            fragments.append(f"[用户] {inp.content}")

    return "nn".join(fragments)

感知层决定了 Agent 能”看到”什么。看到的东西不对、不完整,或者排列顺序让模型注意力分散,后面的规划和执行都会出问题。

规划层:从目标到行动序列

规划层负责回答一个问题:给定当前的目标和上下文,下一步应该做什么?

规划的方式主要有两种:

ReAct(Reasoning + Acting)循环是最常见的模式。Agent 在每一步先”思考”,然后决定一个具体的行动,执行后观察结果,再进入下一轮思考。整个过程是增量的,不需要在开始时就规划出全部步骤。

思考: 我需要找出当前系统的日志文件位置
行动: search_files(pattern="*.log", directory="/var/log")
观察: 找到 3 个文件: /var/log/app.log, /var/log/error.log, /var/log/access.log
思考: error.log 最相关,读取最近 100 行
行动: read_file(path="/var/log/error.log", tail=100)
观察: [文件内容...]
思考: 看到了两个 NullPointerException,定位到 UserService.java:234
行动: ...

Plan-and-Execute(计划-执行分离)是另一种方式:先生成一个完整的步骤序列,然后逐步执行。在任务结构清晰时效率更高,有基准测试数据显示这个架构在某些场景中比 ReAct 快 3.6 倍,因为减少了每步重新推理的开销。缺点是灵活性差,中间某步出了意外,整个计划需要重新生成。

规划层的关键挑战是任务拆解(Task Decomposition):一个高层目标如何被分解成可执行的子任务?分解的粒度太粗,Agent 会在执行时迷失方向;粒度太细,规划本身的成本就很高,而且细粒度计划面对环境变化时更容易失效。

好的任务拆解应该遵循两个原则:每个子任务都有明确的完成标准,且任务之间的依赖关系是清晰的。模糊的子任务会让 Agent 在执行时随机游走,最终偏离原始目标。

记忆层:短期与长期的分工

记忆是 Agent 工程中最容易被低估的问题。一个 Agent 没有适当的记忆系统,它对你就是永远的第一天:每次从零开始,不知道上次做了什么,不记得你的偏好,无法积累经验。

记忆有两种:

短期记忆就是上下文窗口,是 Agent 当前的”工作内存”。正在处理的信息、对话历史、最近几次工具调用的结果,都存在这里。窗口是有限的,即使是 200K token 的窗口,在长任务中也会被填满。满了就要截断、压缩,或者把子任务分给子 Agent。

长期记忆是跨会话持久化的记忆,通常存在文件系统或数据库里。内容可以按需召回,不会一直占用上下文窗口。典型的长期记忆:用户的偏好和习惯、过去任务的执行日志、通过 RAG 检索的知识库。

一个有代表性的三层记忆架构:

第一层(即时注入层):MEMORY.md + USER.md
  - 每次会话自动读入上下文
  - 保存最关键的用户偏好和项目约束
  - 人类可编辑,小而精准

第二层(检索层):全文检索数据库(如 SQLite FTS5)
  - 跨会话的对话历史和任务记录
  - 按需搜索,不全量加载
  - 关键词匹配 + 向量相似度混合检索

第三层(知识图谱层):实体-关系图
  - 自动从对话和文档中提取实体、事实、关系
  - 支持"A 和 B 的关系是什么"这类结构化查询
  - 读 1 篇文档,刷新整个知识网络

记忆有一个被严重低估的腐败问题。记忆不是存进去就一直有效的。随着时间推移,代码更新了、决策变了、项目方向调整了,但记忆里存的还是旧的信息。Agent 越用越笨的根源,往往不是模型退化,而是记忆已经”脑腐”——过期的记忆比没有记忆更危险,因为 Agent 会基于错误的前提做事,而不是意识到信息缺失去询问。

把三层模块串在一起:一个最简 Agent 循环

class MinimalAgent:
    def __init__(self, model, tools, memory):
        self.model = model          # LLM 推理能力
        self.tools = tools          # 可调用工具集
        self.memory = memory        # 记忆系统
        self.max_steps = 20         # 防止无限循环

    def run(self, goal: str) -> str:
        # 从长期记忆中检索相关上下文
        context = self.memory.retrieve(query=goal)

        messages = [
            {"role": "system", "content": build_system_prompt(context)},
            {"role": "user", "content": goal}
        ]

        for step in range(self.max_steps):
            # 规划层:模型决定下一步
            response = self.model.complete(messages, tools=self.tools)

            # 判断是否完成
            if response.finish_reason == "stop":
                # 把本次任务的关键信息存入长期记忆
                self.memory.store(goal=goal, result=response.content)
                return response.content

            # 执行工具调用
            if response.tool_calls:
                tool_results = []
                for call in response.tool_calls:
                    # 感知层:执行工具并捕获结果
                    result = self.tools.execute(call.name, call.args)
                    tool_results.append({
                        "tool_call_id": call.id,
                        "content": str(result)
                    })

                # 把工具结果加回上下文(感知层)
                messages.append(response)
                messages.extend(tool_results)
            else:
                # 模型没有调用工具也没有结束——异常情况
                messages.append(response)
                messages.append({
                    "role": "user",
                    "content": "请继续执行任务,如果已经完成请给出最终结论。"
                })

        # 超过最大步数,强制退出
        return "任务未能在最大步数内完成,请检查任务定义或增加步数限制。"

这个伪代码省略了很多生产级细节,但它展示了 Agent 最核心的运转逻辑:目标驱动、循环执行、工具调用、状态管理、安全退出。


三、行动与执行

感知和规划之后,Agent 需要真正地”做事”。这就是执行层的工作。执行层的核心组件是工具系统,以及规划器和执行器的分工协作。

工具调用:从文字到真实世界的操作

工具调用(Tool Use)是 Agent 与纯 LLM 最显著的分界线。LLM 的输出是文字,而 Agent 通过工具,可以让这些文字产生真实的效果——执行代码、修改文件、发送请求、查询数据库。

从工程实现角度,工具调用分三步:工具定义、工具选择、工具执行。

工具定义是向 LLM 声明哪些工具可用,包含功能、参数类型、参数说明。描述质量直接影响模型的调用准确率。工具选择是 LLM 根据当前任务决定调用哪个工具、传什么参数,这是推理的一部分。工具执行是 Harness 接收调用请求,实际运行工具,把结果返回给 LLM。

一个典型的工具调用格式:

{
  "type": "function",
  "function": {
    "name": "search_web",
    "description": "搜索互联网获取最新信息,适合查找近期事件、最新数据或特定事实",
    "parameters": {
      "type": "object",
      "properties": {
        "query": {
          "type": "string",
          "description": "搜索关键词,尽量精准,避免歧义"
        },
        "max_results": {
          "type": "integer",
          "description": "返回结果数量,默认 5,最多 20"
        }
      },
      "required": ["query"]
    }
  }
}

工具的分类视角很多。从功能维度,常见的工具类型包括:文件系统操作(读/写/搜索文件)、代码执行(运行 Python/bash 脚本)、网络访问(搜索、抓取网页、HTTP 请求)、数据库查询(SQL 查询、向量搜索)、外部 API 调用(发邮件、创建工单、调用业务服务)、子 Agent 调用(把任务委派给专用的 Agent)。

工具的数量不是越多越好。Vercel 的工程团队分享过一个反直觉的经验:砍掉 80% 的工具之后,Agent 的表现反而更好。工具越多,模型需要在工具选择上消耗的推理成本越高,出错的概率也越大。精简工具集、让每个工具定义清晰,往往比提供大而全的工具集更有效。

另一个常见的执行问题是工具调用格式错误。在生产环境中,有两种典型的失败模式:模拟工具调用(模型把 JSON 格式的工具调用写进了文本内容,而不是通过 API 的 tool_calls 字段发出,导致工具从未被触发,但后续推理却基于虚构的执行结果展开);计划性文字(模型输出”好的,我先做 A,再做 B”作为普通文本返回,Harness 误以为任务完成,直接退出循环)。这两种失败在日志里很难区分,必须在 Harness 层做严格检测。

规划器与执行器的分工

复杂的 Agent 系统通常把规划和执行分成两个独立组件。

规划器(Planner)理解目标、拆解任务、生成行动序列。一般用能力更强的模型,因为规划错了代价更高——方向错了,后面做得越多偏得越远。执行器(Executor)按规划器的指令逐步执行,可以用更小更快的模型,某些步骤甚至不调用 LLM,直接走确定性脚本。

分离带来的好处是:规划可以在执行前做完整性检查,执行失败能触发重新规划,两个组件可以独立替换。代价是架构复杂度增加,组件间需要明确的接口和状态传递协议。任务结构固定、流程较长的场景(自动化流水线、内容生产工作流)适合分离;任务结构多变时,ReAct 的单循环往往更务实。

同步执行与异步执行

从时间维度,执行方式有同步和异步两种。

同步执行是 Agent 一步一步等待每个工具调用完成后再继续。实现简单,调试方便,适合大多数场景。异步执行是同时发起多个工具调用,等它们都返回后再合并结果。在需要并行处理多个独立子任务时(比如同时搜索多个数据源),能显著减少等待时间。

# 异步并行工具调用示例
import asyncio

async def parallel_research(topics: list[str]) -> dict:
    # 同时对多个主题发起搜索,而不是逐一等待
    tasks = [search_web(topic) for topic in topics]
    results = await asyncio.gather(*tasks)
    return dict(zip(topics, results))

异步执行的挑战在于结果合并和错误处理。如果某个并行任务失败,需要决定是等待其他任务、立即中止,还是允许部分失败继续。生产级 Agent 通常会给每个并行任务设置超时,并提供降级策略。


四、自主决策的边界

自主性是 Agent 最吸引人的特质,也是最需要谨慎设计的地方。不是所有任务都适合让 Agent 完全自主执行,也不是介入越少就越好。

确定性任务与开放性目标

自主决策的边界,先要区分两类任务。

确定性任务有明确的起点、结束状态和成功标准。”把这个目录下所有超过 1MB 的日志文件压缩归档”就是。Agent 可以高度自主地执行,完成后状态可以验证,错误可以回滚。

开放性目标的成功标准需要主观判断。”提升这个产品的用户留存”是典型的开放性目标。每个决策点都在做假设,每个假设都在累积错误风险。

确定性任务可以设计成全自动流水线。开放性目标通常需要在关键节点引入人类判断,不是因为 Agent 不够聪明,而是这些节点的判断本身需要真实的价值排序,Agent 没有这个能力。

一个实用的分类方法:用两个维度评估任务——可逆性(操作失败了能不能回滚)和影响范围(操作影响的是本地数据还是生产系统或外部用户)。可逆性高、影响范围小的任务,可以给更高自主度;不可逆或影响范围广的操作,需要在执行前引入人工确认。

Human-in-the-loop:什么情况下需要人工介入

Human-in-the-loop 是系统设计的一部分,不是能力不足的补偿。以下几种情况适合主动引入人工介入:

任务有多种合理解读、不同解读会导致截然不同路径时,与其让 Agent 随机选,不如开始前明确。删除数据、发送外部通信、部署代码这类不可逆操作,执行前要有人类审核节点。Agent 连续几步没有取得进展、或推理结果置信度低时,应该主动报告状态请求指导。某些任务的结果质量需要领域专家判断,Agent 执行完不等于结果正确。

介入的设计要考虑成本。每个工具调用都要确认的话,Agent 的效率优势就消失了。好的设计是把介入集中在真正关键的节点。

过度自主的风险:权限蔓延与不可逆操作

自主性高的 Agent 面临两类风险。

一是权限蔓延(Permission Creep)。Agent 在执行过程中,可能逐渐触碰超出原始授权范围的资源。比如”帮我清理重复文件”,可能演化成读取所有目录、删除 Agent 认为是重复但实际有用的文件。每一步单独看都有合理性,但链条终点早就超出了初始授权。对应的防护是最小权限原则:Agent 只能访问完成当前任务必需的资源,权限校验在 Harness 层做,不依赖模型自觉。

二是不可逆操作的级联风险。一个错误决策可能触发一系列不可逆操作。发出的邮件收不回来,删除的数据库记录找不回来,生产环境的错误配置可能引发雪崩。对应的防护是操作前检查点:高风险操作设计成”先生成变更 diff → 人工审核 → 才执行”的模式;文件系统操作在执行前做快照备份;数据库操作先在事务中执行,确认后再提交。

Anthropic 的权限架构有一个原则:把权限执行和模型推理分离。模型决策调用哪些工具,但权限校验不依赖模型的自我判断,由 Harness 层独立完成。这样即使模型决策出错,越权操作也可能在执行层被拦截。


五、幻觉:Agent 最大的内部风险

幻觉(Hallucination)在聊天场景中是一个烦人的问题——模型会编造引用、错误归因、把不确定的事说得煞有介事。但在 Agent 场景中,幻觉的风险被放大了一个数量级:一个幻觉结果可能触发一系列工具调用,每个工具调用都在”真实地”执行错误的前提。

幻觉的三个来源

一是训练分布外。LLM 对训练数据中没有充分覆盖的领域,倾向于用”看起来合理”的内容填充。私有业务数据、最新动态、罕见技术领域,都是幻觉高发地带。模型不是在说谎,是在做统计外推,恰好外推错了。

二是长链推理的误差累积。每一步推理都有概率犯小错误。在单步回答中影响不大,但一个需要 10 步推理的 Agent 任务里,如果每步出错概率是 5%,端到端成功率只有 0.95^10 ≈ 60%,即使每一步看起来都挺合理。每步成功率 99%、任务有 50 步,成功率只剩 60.5%。任务越长,可靠性越脆弱。

三是工具返回值误读。工具调用返回的数据可能格式不规范、内容有歧义、或者包含没有预期的异常状态。模型倾向于从中找到能推进任务的信号,有时会过度解读、选择性忽略异常,把错误码当成正常返回处理。

幻觉在 Agent 中的放大效应

在聊天场景,幻觉的危害是:用户收到了错误的信息。在 Agent 场景,危害是:基于错误信息触发了真实的操作,而这些操作可能是不可逆的。

一个具体的失败链条:

步骤1: Agent 在数据库查询时幻觉出一个不存在的表名
步骤2: 幻觉的 SQL 查询"成功"返回(实际上连接了错误的库)
步骤3: Agent 基于错误数据进行了分析,得出了"合理的"结论
步骤4: 基于这个结论,Agent 调用了外部 API 更新了下游系统
步骤5: 下游系统的数据被污染,但表面上一切看起来正常

整个链条中,没有一步会触发明显的错误报警。Agent 幻觉危险就在这里:后果不是立即显现的崩溃,而是悄悄地污染系统状态。

缓解策略

验证循环(Verification Loop)是在关键步骤之后引入独立的验证步骤。验证可以是规则驱动的(检查输出格式、数值范围、逻辑一致性),可以是工具驱动的(把 Agent 的结论代入工具执行,用执行结果验证推理),也可以是 LLM-as-judge(用另一个模型评估输出质量)。验证循环能将输出质量提升 2-3 倍,代价是增加执行步数和成本。

def verify_step_result(result: str, context: dict) -> VerificationResult:
    """
    对关键步骤的输出进行多维度验证
    """
    checks = [
        check_format(result),           # 格式合法性
        check_consistency(result, context),  # 与上下文的一致性
        check_factual_grounding(result, context["retrieved_docs"]),  # 文档依据
    ]

    failed = [c for c in checks if not c.passed]
    if failed:
        return VerificationResult(
            passed=False,
            feedback="n".join([c.feedback for c in failed])
        )
    return VerificationResult(passed=True)

Grounding(扎根验证)是在 Agent 做出声明或决策之前,强制要求它引用支持该声明的具体来源。没有来源的声明视为可疑,触发额外的搜索或查询。模型要么找到真实来源,要么承认不确定,没有”自信地给出无依据答案”的空间。

置信度阈值(Confidence Threshold)是在关键决策节点要求模型给出置信度估计。低于阈值时不执行高风险操作,先补充信息。实践中不容易精准,模型的自我评估本身也可能偏差,但作为触发”暂停并询问”的信号,比什么都不做有用。


六、目标漂移与死循环优化

幻觉是内容层面的风险。目标漂移和死循环是行为层面的风险:Agent 知道在做什么,但做的方向悄悄偏了,或者陷入了无效的重复循环。

目标漂移:执行过程中目标的悄悄偏移

目标漂移(Goal Drift)是指 Agent 在执行过程中,原始目标被逐渐替换为一个略有偏差的目标,而这个替换发生得很隐蔽,很难被单步检测到。

一个真实案例类型:用户让 Agent 帮忙”优化这段代码的可读性”。执行到第 3 步时,Agent 发现这段代码有一个性能瓶颈。出于”帮用户解决问题”的推理倾向,它开始着手优化性能。性能优化需要改变算法,算法改变了可读性又降低了。到第 7 步时,代码的可读性可能比原来更差,但 Agent 觉得它做了一件更重要的事。

目标漂移的成因通常是:中间步骤产生的发现改变了 Agent 对”更好目标”的估计。这不完全是错误的,有时中途调整是合理的。问题在于调整发生时没有被用户确认,用户交付的任务和实际执行的任务已经不是同一件事。

防护机制:在 Agent 循环的每个主要阶段,显式检查当前行动是否仍然对齐初始目标。可以用一个简单的结构化检查:

def goal_alignment_check(
    original_goal: str, 
    current_action: str, 
    context: dict
) -> bool:
    """
    检查当前行动是否仍然对齐初始目标
    """
    prompt = f"""
    初始目标: {original_goal}
    当前计划执行的操作: {current_action}

    问题:当前操作是否直接服务于初始目标?
    如果是,解释怎么服务。
    如果不是,说明为什么认为这个偏转是合理的,并列出这个偏转需要用户确认的原因。

    用 JSON 回答: {{"aligned": true/false, "reason": "...", "needs_confirmation": true/false}}
    """
    result = llm.complete(prompt)
    return parse_json(result)["aligned"]

死循环优化:Agent 陷入局部最优的三种模式

Agent 陷入死循环或局部最优的模式比预期中常见,通常有三种:

模式一:错误重试循环。工具调用失败,Agent 用几乎相同的参数重试。每次失败,Agent 稍微调整一下参数,然后继续重试。10 步之后,Agent 仍在重复同一个本质上失败的操作,只是参数略有变化。

模式二:信息收集无底洞。Agent 发现要完成任务需要更多信息,开始搜索。搜索返回的结果提示了更多需要了解的方向,于是继续搜索。20 步之后,Agent 积累了大量背景信息,但任务的核心工作一步都没有推进。

模式三:计划-重计划循环。Agent 生成一个计划,开始执行第一步时遇到障碍,于是重新规划。新计划执行第一步又遇到障碍,再次重新规划。每次重新规划都会稍微调整,但核心矛盾没有被解决,Agent 在计划层面反复循环,从不真正执行。

防护机制需要从三个维度设计:

class LoopDetector:
    def __init__(self, max_steps=20, state_window=5):
        self.history = []
        self.max_steps = max_steps
        self.state_window = state_window

    def check(self, current_state: dict) -> LoopCheckResult:
        # 检查1: 最大步数
        if len(self.history) >= self.max_steps:
            return LoopCheckResult(
                should_stop=True,
                reason=f"已达最大步数 {self.max_steps},强制退出"
            )

        # 检查2: 状态变化检测
        recent = self.history[-self.state_window:]
        if len(recent) >= self.state_window:
            state_changes = [
                state_diff(recent[i], recent[i+1]) 
                for i in range(len(recent)-1)
            ]
            if all(change < STAGNATION_THRESHOLD for change in state_changes):
                return LoopCheckResult(
                    should_stop=True,
                    reason=f"最近 {self.state_window} 步状态变化不足,可能陷入局部最优"
                )

        # 检查3: 重复行动检测
        recent_actions = [s.get("last_action") for s in recent]
        if len(set(str(a) for a in recent_actions)) <= 2:
            return LoopCheckResult(
                should_stop=True,
                reason="检测到重复行动模式,可能陷入循环"
            )

        self.history.append(current_state)
        return LoopCheckResult(should_stop=False)

这三个检查——最大步数、状态变化检测、重复行动检测——覆盖了大多数生产中遇到的死循环模式。没有这类防护机制,一个 Agent 在计费系统中很容易在循环中烧掉几十倍于预期的 token 成本。


七、一个完整的 Agent 闭环

把前面所有模块串在一起,用一个具体的场景来展示完整的 Agent 运行过程:处理一张客服工单。

场景设定

一家 SaaS 产品收到一张工单,内容是:”我们的 API 密钥昨晚突然失效,服务中断了两个小时,希望说明原因并退款。”

Agent 的任务是:分析工单、查询相关记录、起草回复、若符合退款条件则走退款流程。

完整执行流程

节点 1:接收与感知

工单系统触发 Agent,传入工单 ID。感知层读取工单内容、关联客户信息、该客户的历史工单记录、近期服务状态公告。

这一步最容易出的问题:上下文加载不完整。如果 Agent 只读到了工单文本,没有关联到客户账户信息,后面的处理就会产生大量无意义的查询。

节点 2:规划

规划层将任务拆解为子任务序列:
1. 核实客户身份与账户状态
2. 查询昨晚该时段的服务状态日志
3. 判断是否存在已知服务中断事件
4. 评估退款申请是否符合 SLA 条款
5. 生成回复草稿
6. 如果退款条件满足,触发退款工单

这是一个计划-执行架构的典型场景:任务结构清晰,子任务之间有顺序依赖,先规划后执行比 ReAct 更高效。

节点 3:记忆查询

执行子任务 1 之前,记忆层被查询:这个客户此前有没有提交过类似工单?有没有特殊备注?上次交互的结论是什么?如果这是一个 VIP 客户,相关标记应该已经在记忆层中,这一步会把该信息注入当前上下文。

节点 4:工具调用序列

工具调用 1: query_customer(customer_id="xxx")
  返回: 账户状态=活跃, 计划=Pro, API密钥最后更新=3天前

工具调用 2: query_service_log(time_range="2026-05-28 23:00~01:00", service="auth")
  返回: 2026-05-29 00:12~01:47 存在认证服务降级,影响范围=全量用户

工具调用 3: query_sla_policy(customer_plan="Pro", incident_duration=95)
  返回: Pro计划SLA=99.5%月可用率, 95分钟中断=符合退款补偿标准

工具调用 4: draft_reply(context={...})
  返回: [回复草稿文本]

节点 5:结果验证

生成回复草稿后,验证循环检查:
– 回复是否提到了服务中断的具体时间段?(事实核查)
– 退款金额计算是否符合 SLA 条款的公式?(规则检查)
– 回复语气是否符合客服规范?(格式检查)

如果验证失败,Agent 会收到失败反馈,重新生成回复,而不是直接发出。

节点 6:人工节点(可选)

如果退款金额超过阈值(比如超过 1000 元),或者客户账户标记为”高风险”,系统自动暂停,把回复草稿和退款申请推入人工审核队列。Agent 附上完整的执行过程和判断依据,供审核人员快速决策。

节点 7:执行与关闭

审核通过后(或金额在自动处理阈值内时),Agent 调用退款工具,更新工单状态为”已解决”,记录本次处理过程到记忆系统,工单关闭。

生产中最容易出问题的节点

感知层的信息缺口:工具查询返回不完整数据时,Agent 有时会用”合理推断”来填补,而不是报告数据缺失。这是早期幻觉的高发点。解决方案是在感知层为工具返回值设置必填字段校验,缺失时强制返回错误而非空值。

规划的假设失效:上面的流程假设服务日志是可查询的。如果日志系统在工单处理时也出了故障(这在真实事故中不罕见),计划会在节点 4 卡住。好的 Agent 设计需要为每个关键步骤定义失败时的降级路径:日志查不到,怎么处理?是升级给人工,是用其他数据源替代,还是在回复中如实说明?

长链推理的漂移:当工单内容复杂(包含多个问题、跨越多个账户)时,Agent 可能在处理某个子问题时逐渐遗忘了原始工单的其他部分。解决方案是在每个主要子任务完成后,对原始工单重新做一次覆盖检查——所有问题都被处理了吗?

记忆的陈旧信息:如果记忆层记录了该客户”偏好邮件通知而非工单回复”,而这条记录已经是半年前的,Agent 可能会因为陈旧偏好设置而做出错误的沟通决策。记忆有效期管理是生产级 Agent 工程中一个持续需要维护的问题。


八、可观测性:Agent 工程的隐藏基础设施

构建 Agent 和维护 Agent 是两件截然不同的事。很多团队在演示阶段表现良好,进入生产后很快陷入一个困境:Agent 出了问题,但不知道问题在哪里,也不知道是哪一步出错的,更不知道怎么复现。这个困境的根源是可观测性缺失

可观测性(Observability)对 Agent 来说比对普通微服务更重要,原因在于 Agent 的执行路径是动态的:同样的输入,不同时间可能走完全不同的推理路径和工具调用序列。传统的”查日志、看指标”不够用,需要能追踪每一次决策的完整上下文。

一个可观测的 Agent 系统需要记录三类关键信息:

决策痕迹:每次规划层做出决策时,记录下当时的上下文输入、模型输出的推理过程、选择的工具和参数。这让你事后能重建”它当时看到了什么、想到了什么、决定做什么”。

工具调用日志:每次工具调用的完整记录——调用的工具名、输入参数、返回结果、执行时间、是否发生错误。工具调用是 Agent 与外部世界的唯一接触点,这里的记录是故障排查的第一线。

状态快照:在循环的关键节点,保存完整的 Agent 状态快照。包括当前目标、已完成的子任务、当前上下文的摘要、记忆层的状态。这让你可以在出问题时”回放”Agent 的执行过程。

class ObservableAgent:
    def __init__(self, agent, tracer):
        self.agent = agent
        self.tracer = tracer   # 追踪器(如 LangSmith、自定义日志系统)

    def run(self, goal: str, trace_id: str = None):
        with self.tracer.span("agent_run", goal=goal, trace_id=trace_id) as span:
            for step_result in self.agent.step_iter(goal):
                # 记录每一步的完整状态
                self.tracer.log_step(
                    span=span,
                    step_num=step_result.step_num,
                    reasoning=step_result.model_reasoning,    # 模型内部推理
                    action=step_result.action,                # 决策的行动
                    tool_calls=step_result.tool_calls,        # 实际工具调用
                    observations=step_result.observations,    # 工具返回值
                    state_hash=hash_state(step_result.state)  # 状态指纹
                )

                if step_result.done:
                    span.set_result(step_result.final_output)
                    break

可观测性的价值不仅是事后排查。在有了足够的执行数据之后,你可以建立评测集——把真实失败案例整理成测试用例,用来衡量 Agent 改进后是否真的变好了,还是只是这次测试运气好。没有可观测性,跑测试集只是白白消耗 token,因为你看不到在哪些步骤失败、失败的模式是什么。这正是从 demo 走向可维护生产系统的分水岭。

Harness 的厚度选择

工程实践中有一个反直觉的现象:功能越多的 Agent 框架,不一定让 Agent 表现得更好。Anthropic 内部有一个”薄 Harness”的倾向——尽量把复杂度保留在模型层,而不是用越来越多的 Harness 代码来弥补模型能力的不足。

这个倾向有一定道理:Harness 代码越厚,可能掩盖的问题越多。如果一个 Agent 只有在 Harness 加了大量特殊处理后才能可靠运行,那本质上是在用工程技巧掩盖模型的短板,而不是真正解决了问题。随着模型能力提升,这些特殊处理可能反而成了约束。

但”薄 Harness”不是不要 Harness。验证循环、权限校验、死循环防护、可观测性,这些是不应该省略的基础设施,不管模型能力多强。真正应该保持薄的是任务特定的胶水代码:那些为了让特定场景勉强跑通而堆叠的 if/else 和 prompt 魔法。

Manus 团队的经验值得参考:他们在六个月内把自己的 Agent 系统重写了五次。每次重写的方向都是砍掉复杂度,而不是增加功能。每次砍掉一批特殊处理代码之后,在更好的模型上性能反而提升了。这是”Harness 应该随着模型进步而退化”这个原则的实证。


九、多 Agent 系统的协作边界

单个 Agent 的能力有边界:上下文窗口有限、注意力随长度衰减、单线程执行速度受限。在任务规模超出单 Agent 能处理的范围时,多 Agent 协作是一个自然的扩展方向。

但多 Agent 系统不是免费的午餐,它引入了单 Agent 没有的新问题类型。

三种常见的多 Agent 模式

Fork 模式(子 Agent 委派):主 Agent 把子任务委派给专用 Agent 执行,子 Agent 完成后把结果返回给主 Agent。这是最简单的多 Agent 模式,适合”任务可以被清晰分解、子任务相互独立”的场景。

主 Agent
├── 子 Agent A: 负责搜索和数据收集
├── 子 Agent B: 负责代码生成和测试
└── 子 Agent C: 负责文档整理和输出

Teammate 模式(平行协作):多个 Agent 平行工作,彼此可以通信。适合需要多角色协作的场景,比如”一个 Agent 写代码,另一个 Agent 做代码审查”。这种模式下,Agent 之间的通信协议和权限边界需要仔细设计。

Worktree 模式(并行探索):同一个任务,启动多个 Agent 从不同角度探索,最后合并最优路径。适合搜索空间大、正确路径不确定的问题。代价是资源消耗是单 Agent 的 N 倍。

多 Agent 的三个典型坑

文件冲突:多个 Agent 同时修改同一份文件,互相覆盖对方的修改。这是最直接的协作问题,解决方案是引入任务认领机制——每个 Agent 开始修改资源前先”锁定”它,完成后释放。Git commit 是一个自然的检查点机制,把文件系统的 commit 作为 Agent 操作的原子单位。

经验不迁移:Agent A 在执行任务时积累的经验(踩过的坑、找到的有效路径)不会自动传递给 Agent B。两个 Agent 可能在同一个地方分别踩坑,浪费资源。解决方案是在多 Agent 系统中引入共享的经验文档层,每个 Agent 在任务结束时把有价值的发现写入共享记忆。

安全劫持(Prompt Injection):Agent 在执行过程中会读取外部内容——网页、文件、数据库记录。这些内容可能包含精心构造的”指令”,试图劫持 Agent 的行为。单 Agent 场景下这已经是问题,多 Agent 场景下一个被劫持的 Agent 可能通过通信协议感染其他 Agent。防护机制是在 Harness 层对 Agent 读取的外部内容进行清洗,限制外部内容中的指令格式,以及对 Agent 间通信建立信任等级。

从组织视角看,多 Agent 系统面临的挑战和团队协作的挑战高度同构:
– 文件冲突 = 职责边界不清
– 经验不迁移 = 知识管理缺失
– 安全劫持 = 访问控制缺失

好的多 Agent 系统设计,往往需要借鉴已有的组织管理经验,而不只是技术经验。


十、从演示到生产:四条结构性鸿沟

Agent 系统从”演示好用”到”生产可靠”之间,有四条结构性鸿沟。了解这四条鸿沟,是避免在生产阶段遭遇意外的前提。

鸿沟一:LLM 注意力的不均匀分布

演示场景中,任务短、上下文小,模型的注意力能覆盖所有关键信息。生产场景中,上下文随任务推进不断增长,模型对早期约束条件的”记忆”会随长度增加而衰减。

一个具体表现:在 skill 或 CLAUDE.md 中定义的某个约束(比如”所有输出必须包含引用来源”),在任务的前三步会被严格遵守,到了第八步可能已经被”遗忘”,模型开始输出无来源的内容。模型并非主动决定跳过,而是注意力稀释在概率层面的衰减效应。

工程上有几种缓解方法:把关键约束重复注入上下文(每隔几步在系统提示中重申一次);把长流程拆分为独立的短子流程,每个子流程有独立的完整上下文;在长流程的关键节点强制做约束检查,而不是依赖模型自觉遵守。

鸿沟二:工具调用的可靠性

演示场景中,工具通常按预期工作,返回结构化的结果。生产场景中,工具会超时、返回格式不稳定、依赖的服务偶发故障、API 限额被触发。

一个 Agent 对工具错误的处理质量,往往决定了它在生产中的实际可用率。演示时工具总是成功,所以错误处理路径从未被测试过。真正上线后,工具错误是日常,而不是例外。

生产级 Agent 的工具层需要:为每个工具定义错误类型和对应的处理策略(重试、降级、人工升级);给每个工具调用设置超时,超时后主动中止而不是无限等待;在工具返回异常结果时,不让异常悄悄传播到后续推理,而是在感知层显式处理。

鸿沟三:测试覆盖的盲区

演示场景中,通常只测试”理想路径”——所有工具都成功,模型做出正确决策,任务一步步走到完成。生产场景中,边界情况才是主角。

Agent 的测试比普通代码测试更难:执行路径是动态的,同样的输入不一定每次都走相同的路径;LLM 本身有随机性,同一个输入运行十次可能有不同的工具调用序列。

实用的 Agent 测试策略不是追求 100% 路径覆盖,而是聚焦在已知的失败模式上:从生产日志中收集失败案例,整理成回归测试集;对关键决策节点的行为做快照测试(节点的输入输出组合);用 LLM-as-judge 对生成内容做自动质量评估,建立基线分数。

鸿沟四:成本和延迟的现实

演示场景中,成本和延迟通常不是考量维度——偶尔跑一次,等几秒甚至几分钟都可以接受。生产场景中,成本和延迟直接影响系统的可用性和商业可行性。

Agent 系统的成本控制有几个常见手段:在不需要完整推理能力的步骤(格式转换、数据提取、简单判断)用更小更快的模型;对高频重复的工具查询结果做缓存,避免重复调用;在感知层做更激进的上下文压缩,用更少的 token 表达同样的信息。研究显示,激进的上下文压缩可以减少 26-54% 的 token 消耗,同时保留 95% 以上的任务准确率。

延迟的主要来源是串行工具调用——每个工具调用都需要等待上一个完成。识别可以并行化的工具调用,是降低延迟最有效的手段。但并行化需要仔细判断哪些调用之间有依赖、哪些真正独立。


结语

我见过的大多数 Agent 翻车,不是因为模型不够聪明,而是因为没有想清楚”失败的时候会发生什么”。

动手之前,先把三件事写下来:任务的成功定义(什么叫完成了?)、失败画像(哪些情况算出错?)、不可逆操作清单(哪些操作一旦执行就没有后退?)。这三样东西写清楚了,Harness 怎么设计、哪里加人工节点、哪里加验证循环,自然就有了答案。

没有这三样东西就开始写代码的 Agent,通常会在演示阶段跑得很好,然后在生产里以各种奇怪的方式失败。


「Agent 知识丛书」系列第 1 篇,共 9 篇。

抢沙发

评论前必须登录!

立即登录   注册