前言
近期观察到一个明显趋势:在暑期实习招聘中,多家大厂已极少设置传统的前后端、测开等研发岗位,招聘需求几乎全部集中在AI研发工程师方向。不了解情况的人,可能会误以为这些企业招的全是算法岗。与此同时,面试环节也开始实实在在考察AI Coding能力,而传统的八股文知识反而不再那么受重视。虽然尚不确定其他企业是否会全面跟进这一变化,但提前学习AI Agent相关知识,总归是有益无害的。本文基于笔者浏览多篇资料后的梳理,旨在构建一套从原理、架构到最佳工程实践的对Agent的理解框架,希望能为读者带来切实帮助。
本篇文章将重点剖析以下几个核心问题:究竟什么是Agent?并结合工程实践,深入拆解对Agent整体能力影响最大的几个关键模块——Agent Loop(智能体循环)、Context Engineering(上下文工程)、Tool(工具系统)、Memory(记忆系统)、Muti-Agent(多智能体协作)以及Agent Tracing & Evaluation(智能体追踪与评估)。此外,还会分享如何编写高效的Prompt,以及如何正确构建Skill(技能)。全文内容均为实实在在的实战干货。
什么是Agent?简单抽象一下Agent Loop
所谓Agent,本质上是一套能够感知环境、自主决策并采取行动以完成目标的AI系统。它包含四大核心要素:
- 感知(Perceive):接收各类输入信息,包括文字、图片、工具返回的结果等
- 思考(Think):以LLM(大语言模型)作为思考核心,进行推理、规划与决策
- 行动(Act):调用各类工具,执行代码、执行搜索、使用操作系统能力等
- 记忆(Memory):保存上下文信息,执行多步骤任务,并在任务中断时恢复现场
相较于普通大模型那种“单次对话的聊天框模式”,Agent最显著的特征在于它能够调用工具去自主完成任务。以下这个比喻一直被认为非常贴切:
Agent 工具能力的来源
大语言模型(LLM)是整个Agent系统的“大脑”,负责下达指令,就好比人脑一样。而系统提供给它的各类广义工具,则相当于Agent的“手脚”和“眼睛”。只有将这两部分组装起来,才能构成一个正常且具有自主行动能力的个体。开发Agent,本质上就是帮助LLM“长出”手脚与眼睛。如果没有工程层面提供的这些工具支持,模型再强大,也仅仅是一个高智力的“瘫痪患者”。工具的设计直接决定了Agent能做什么,以及它能走多远。
工具和LLM之间是如何交互的?
当我们说一个AI Agent“调用了工具”时,这句话背后究竟发生了什么逻辑?
LLM本质上是一个文本生成模型,其输入是被处理后的token序列(自然语言),输出同样是token序列。它无法直接执行代码、访问文件系统或调用HTTP接口。那么,工具调用是如何实现的?答案是:通过协议约定与文本解析。
工程侧在调用LLM之前,会在System Prompt或特定的API字段中告知模型:“你可以使用以下工具,当你需要使用工具时,请按照指定格式输出。”模型在生成回复时,如果判断需要借助工具,就会按约定格式输出一段结构化文本。工程侧会拦截这段文本,从中解析出工具名称和参数,然后执行相应工具,最后将结果返回给模型,继续后续对话。这便是整个交互的核心闭环。
工具调用的完整流程如下:
用户输入 ↓
工程侧构建 Prompt(包含工具定义) ↓
调用 LLM,获取模型输出 ↓
解析输出,判断是否包含工具调用 ↓
[是] 执行工具,获取结果 ↓
将工具结果注入对话上下文 ↓
再次调用 LLM,获取最终回复 ↓
输出给用户
这个循环可以执行多轮,直到模型认为不再需要调用工具为止。这就是Agent Loop的一个简单示例。
协议约定:LLM 与工程侧的契约
协议约定是整个工具调用机制的基础。工程侧需要在Prompt中明确告诉模型:
- 有哪些工具可用(工具名称、功能描述)
- 每个工具接受什么参数(参数名、类型、是否必填)
- 调用工具时应该输出什么格式(结构化文本的具体格式)
模型在训练或微调阶段,已经学会了识别这类指令并按格式输出。这已是各大模型厂商在Raw Model层面就做好的工作,使用时无需关注其背后的细节。
目前在工程实践中,主要存在以下几类协议形态:
原生 Function Calling(OpenAI 风格):OpenAI在API层面提供了tools字段,工程侧以JSON Schema的形式描述工具。模型需调用工具时,会在响应的tool_calls字段中返回结构化的调用信息,而非在文本内容中。这是目前最规范的方式,解析成本也最低。
XML 标签协议:在缺乏原生Function Calling支持或需要更灵活控制的场景下,工程侧会约定让模型用XML标签包裹工具调用。Anthropic的早期版本以及众多开源Agent框架(如大名鼎鼎的Bolt.diy)都采用这种方式。
JSON 代码块协议:另一种常见方式是约定模型在Markdown代码块中输出JSON。
ReAct 格式协议:ReAct(Reasoning+Acting)是一种更早期的协议,模型先以自然语言描述思考过程,然后用固定前缀标记动作。工程侧通过正则匹配Action:和Action Input:来解析。
目前最常用的是JSON和XML两种格式。
| 维度 | JSON | XML |
|---|---|---|
| 解析复杂度 | 低 | 中 |
| 特殊字符处理 | 需要转义 | CDATA 支持 |
| 流式解析 | 困难 | 容易 |
| 模型生成稳定性 | 较高 | 高 |
| 与现有工具链集成 | 极好 | 一般 |
一个小示例
下面是一个基于XML形式的工具调用的完整示例代码,可以清晰地看到:构建包含工具定义的Prompt、解析模型输出中的XML工具调用、执行本地Shell命令、将执行结果注入上下文让模型感知结果,以及一个简化的Agent Loop。
(此处为原文章的代码示例,完整保留)
从这里能直观地看出来,看似与Agent交流时只发出了一句话,实际上LLM已经接收到了多次指令,但在用户看来就是一次任务执行。这里的控制完全依赖工程侧的能力。此外,历史状态的维护,也就是下文将要讲到的上下文工程和记忆系统。
模型感知工具结果的关键
通过上面的示例可以看到,工具结果注入上下文后,模型在下一轮调用时,会把这部分内容作为对话历史的一部分来读取。这就是模型“感知”工具执行结果的本质:并非实时感知,而是通过上下文传递。
这里需要着重提几个工程实践要点:
- 结果格式要清晰:模型需要能从结果中理解执行是否成功,以及输出的具体内容。结构化的XML或JSON比纯文本更可靠。
- 错误信息要完整:当工具执行失败时,stderr和exitCode都应传递给模型,以便它判断是否需要重试或更换方式。
- 输出长度要控制:Shell命令可能输出大量内容,需要截断或摘要后再注入,避免超出模型的上下文窗口。
- 多轮工具调用:模型可能在一次任务中连续调用多个工具,每次都需要把结果注入后再继续,这正是Agent主循环存在的意义,也是整体看起来像一次会话的关键。
工具设计的成长
Agent的工具设计并非一开始就有清晰的方法论,而是随着工程实践的深入逐步成熟的。
从“系统视角”到“Agent 视角”
早期(2022年末至2023年),工具设计的主流做法是API封装:系统能做什么,就把什么暴露出来。这种做法的问题是工具粒度细、数量多,LLM需要自行规划完整的调用链,导致上下文压力大,出错率高。
后来,普林斯顿的SWE-agent论文提出了Agent-Computer Interface(ACI)的概念,核心是一次视角转换:工具不应以系统为中心设计,而应以Agent为中心设计。具体来说,工具的命名和描述要贴近LLM的语言理解习惯,粒度要与Agent的目标动作对齐,错误反馈也要主动设计成LLM能够理解并自我纠正的格式。论文实验表明,在其任务设定下,ACI设计对完成率的影响十分显著——工具界面的质量本身就是一个关键变量。
工程实践的进一步深化
视角转换之后,工程上还有一批具体问题需要解决。Anthropic在其技术文档中将这一阶段的实践归纳为Advanced Tool Use,主要包括三个方向:
- Tool Search(动态工具发现):不再把全量工具塞入上下文,而是按需加载,降低噪声与开销。
- Programmatic Tool Calling(代码化调用):让LLM用代码而非多轮对话来编排工具调用,利用循环、条件、变量复用等逻辑结构,使中间工具的执行过程被代码执行屏蔽,不进入LLM上下文干扰,从而减少累积误差。
- Tool Use Examples(调用示例):在工具描述中附带真实调用示例,弥补JSON Schema只能描述结构约束、无法传达典型用法和上下文语义的不足。
详细说说ACI:工具设计的原则
命名与描述贴近 LLM 的语言习惯
工具的名称和描述是LLM选择工具的主要依据。命名要语义直观,描述要讲清楚何时用、怎么用、会返回什么,而不是简单复制底层API的字段名。
(此处为原文章的代码示例,完整保留)
粒度与 Agent 的目标动作对齐
工具粒度并非越细越好或越粗越好,而是要与Agent实际需要完成的一个完整动作对齐。
(此处为原文章的代码示例,完整保留)
主动设计错误反馈,让 LLM 能自我纠正
这是ACI与普通API设计最大的区别之一。工具应捕获错误,并以结构化、可理解的方式返回,告诉LLM哪里错了、应如何修正。
(此处为原文章的代码示例,完整保留)
错误信息直接告诉LLM下一步该调用什么,从而形成自我纠正的闭环。
工具之间保持边界清晰,无功能重叠
如果两个工具的功能存在重叠,LLM在选择时会产生困惑,导致不稳定的调用行为。每个工具应有且只有一个明确的职责。
(此处为原文章的代码示例,完整保留)
工具尽量设计为幂等
幂等意味着同一个调用执行多次,结果与执行一次相同。对Agent来说,幂等工具在出错重试时更安全,状态管理也更简单。
(此处为原文章的代码示例,完整保留)
小小总结
这五条原则的共同出发点是:把LLM当作工具的真正用户来设计,而不是把工具设计成给人看的API文档。
| 原则 | 核心问题 | 设计目标 |
|---|---|---|
| 命名与描述 | LLM 能否选对工具 | 语义直观,说清楚用途和返回值 |
| 粒度对齐 | LLM 调用几次能完成任务 | 一个工具对应一个完整动作 |
| 错误反馈 | LLM 出错后能否自我纠正 | 返回可读错误 + 纠正建议 |
| 边界清晰 | LLM 选工具时是否会困惑 | 无功能重叠,职责唯一 |
| 幂等性 | LLM 重试时是否安全 | 多次调用结果一致 |
小总结
LLM与工具的交互,本质上是一套文本协议、解析执行与结果回注的工程机制。模型不直接执行任何操作,它只负责按照约定的格式输出意图;工程侧负责解析意图、执行操作、感知结果,并通过上下文把结果告知模型。
这套机制看似简单,但在实际工程中,协议的稳定性、解析的容错性、工具的安全边界以及上下文的管理策略,都是需要认真对待的工程问题。理解这个底层机制,是构建可靠Agent系统的起点。
Agent Loop
上面其实已经看到了Agent Loop的概念和简单例子。这里再抽象一下,其实就是四步:感知 -> 决策 -> 行动 -> 反馈。
简化一下之前工具调用里写的那个Agent Loop,本质上就是这几行:
(此处为原文章的代码示例,完整保留)
无论后来增加了什么新概念,比如Skill之类,循环本身都非常稳定。即使从最小实现一路升级到支持上下文压缩、Sub Agent和Skill,循环里的核心内容都不会变动。新增的能力都加在了循环的外围。
扩展能力的方式只有三种:注册新工具和对应的handler、修改系统提示、把需要持久化的状态移至外部存储。循环体本身不应承担状态管理的职责——模型负责推理和决策,状态与边界则交给外部系统。分工一旦清晰,核心循环就会趋于稳定,几乎不需要随能力变化而改动。
Agent只要里面的模型越贵它就越好吗?Harness说:“我不这么觉得。”
Harness(马具/挽具)是近期被讨论得较多的概念,说白了,它就是套在LLM外面的“执行环境”。
通过上述说明也能看出,对于一个Agent而言,脑子好用固然重要,但决定它好不好用的最重要因素是稳定性——具体来说,就是在复杂任务执行时是否足够准确,遇到错误时能否正确处理,以及边界情况能否得到保证。Agent可能在单次执行时成功,但在真实业务中多次运行结果迥异,甚至边界情况下完全失控。这并非模型不够聪明,而是缺乏对行为的有效约束,即缺少一个合适的、能完美控制它的Harness。
Harness具体包含什么
| 模块 | 作用 |
|---|---|
| 工具注册与调用 | 搜索、代码执行、数据库查询 |
| 状态与记忆管理 | 保存对话历史、中间变量 |
| 循环控制 | 控制执行步数、设置终止条件 |
| 错误处理与重试 | 工具失败后如何恢复 |
| 多 Agent 编排 | 调度子 Agent、汇总结果 |
| 安全防护 | 防止越界操作、上下文压缩 |
显而易见,这也是Agent所需的核心能力。这个概念要求了:
- 从文本到系统:约束不再依赖提示词,而是嵌入系统架构,用代码和运行时控制实现。
- 从指令到环境:不是“让AI按指令做”,而是“AI只能在特定环境里做”。
- 从禁止到无法发生:不是“禁止AI做错”,而是“让AI根本做不了错误的事”。
第三条尤其值得展开。之前约束Agent行为,更多是在提示词中给出各种边界,告知哪些要做、哪些不要做。但随着任务执行和上下文累积,LLM可能出现注意力偏移,之前明确说明“不要做”的事情,可能又被执行起来,而该做的事反而被遗忘,尤其在编码规范方面。这里有一个较好的实践:与其在提示词中声明,不如直接写入Linter或Hook中,强制约束要做与不要做的事情,并且不仅给出报错,还应给出如何修正的指引。
(此处为原文章的ESLint自定义规则代码示例,完整保留)
这样一来,在提交代码时,配合Git Hook,Agent即使写出有问题的代码也无法提交,只能老老实实检查报错、找到原因并根据指引修复。这从根本上杜绝了Agent忘记约束的情况。
总体而言,Harness不提供智能,它只是让智能能够稳定运行。它是让Agent从“能做”迈向“稳做”的系统基层基础设施。
上下文工程才是保证Agent稳定第一要素
目前几乎所有常见LLM的底层都是Transformer,其核心在于注意力机制。注意力机制的复杂度是,这意味着随着上下文增长,关键信息所获得的注意力会被各种无用的噪声信息稀释。这也是为什么,随着任务推进,Agent的决策质量会逐渐下滑。这种现象通常被称为Context Rot(上下文腐烂)。在Agent使用过程中,许多看起来像模型能力不足的问题,其背后的元凶往往是上下文组织不当。
怎么写一个好的Prompt
这里插播一下关于如何编写高质量Prompt的内容。因为上下文工程,本质上就是在做Prompt工程。Prompt是与LLM交互的唯一途径——在Agent体系中,除了通过工具扩展模型的行动边界之外,所有对模型行为的影响和干预,最终都要落到Prompt上。因此,写好Prompt,不只是一种技巧,更是一个工程问题。
先理解:Transformer 注意力的“记忆偏好”
想象你在阅读一份很长的文件:
- 开头:你最专注,印象最深
- 中间:容易走神,细节容易忘
- 结尾:刚读完,还留在脑子里
Transformer的注意力机制也有类似的规律:
注意力权重分布(示意)
开头 ████████████ <- 权重高(位置编码强,语义锚定)
中间 ████░░░░░░░░ <- 权重衰减(容易被稀释)
结尾 ████████░░░░ <- 权重较高(proximity effect)
明显可以看出,开头和结尾是Prompt的黄金地段,尤其是开头,会成为整个生成过程的“语义锚”,影响模型对后续所有内容的理解方式。那么,什么是好的Prompt?它应具备以下特征:
- 明确利用注意力权重的分布机制,将核心信息置于头部和尾部,中间则放置相关的上下文补充。
- 信息密度高,没有冗余:不要写成“你好,麻烦你帮我看一下,我有一个问题想请教你,就是关于 React 的,我最近遇到了一些困难……”这样的形式。与人类交流这样没问题,但与LLM交流时,客气和礼貌只会成为分散其注意力的干扰。
- 结构清晰,边界分明:不要写成“你是前端工程师帮我分析一下我们项目用的React18 日活50万大促的时候会卡顿帮我优化”这样的混乱语句。
简单整理就是以上三点,形成一个简洁的优秀三段式Prompt(角色定义+背景+任务要求):
(此处为原文章的Prompt示例,完整保留)
一开始我也以为开头写上角色没什么用,但实践下来,效果确实有可感知的提升。要理解背后的原因,需要先了解LLM的工作方式。
输入的文字先被拆分成一个个Token,每个Token经过模型内部的Embedding层,被映射为一个高维向量,再经过多层Transformer的处理,最终形成携带上下文语义信息的表示。这些向量存在于一个巨大的高维语义空间中,语义越相近的内容,向量的方向就越接近,余弦相似度越高。
需要特别说明的是:这些向量的每个维度没有人类可以直接解读的固定含义(比如“第1维 = 红色”),语义是由所有维度共同分布式编码的,是模型在海量数据中自动学习出来的结果,这与人工设计特征有本质区别。
角色定义的作用,正是在这个语义空间中提供一个上下文锚点。它作为Prompt的一部分输入模型后,会影响模型对后续每个Token的条件概率分布——相当于在概率空间中施加了一个方向性的偏置,引导模型优先激活与该角色相关的知识、语气和表达风格。角色描述越具体,这个偏置越精准,模型输出偏离预期的概率也就越低。
YY一下
好,这就是如何编写一个好的上下文。其实这里说得比较浅显。有人认为,更优的上下文甚至会说明执行上下文压缩时,哪些部分应完整保留原始内容(比如要做与不要做的事项),哪些部分应被完全舍弃(比如工具执行的结果)。
我还想象过,未来面试时,面试官是否会让你讲讲日常工作中自己觉得写过最好的prompt是怎样的?或者给你一个有问题的场景,看你能否根据问题表现,借助Agent在最短时间内描述清楚问题并修复它?再或者,给你完整的PRD、设计稿和技术文档,让你从头使用Agent在最短时间内搭建出一个可用的实现?
上下文分层设计
回到Agent上下文设计本身。最近泄漏的Claude Code源码中,上下文采用了分层设计。结合LLM自身特点,分层设计的上下文是目前的一个最佳实践。正如上文所述,许多情况下,Agent中问题的根源并非模型的上下文窗口不够长,而是模型被无用信息牵制了注意力。在继续之前,需要理解一个事实:如果只是单纯地将所有历史上下文都塞给模型,一些仅在偶然情况下才会用到的内容也会被每次都加载进来,导致稳定的规则约定与动态的工具调用信息、状态信息混杂在一起,大大稀释了上下文中的有用信息浓度。LLM看到的内容越来越多,但真正有用的信息所获得的注意力却越来越少。
为了解决这个问题,将上下文进行分层,按照使用频率和稳定性进行拆分,是一种比较优秀的方案。
判断一条内容是否应该进入上下文,核心问题只有一个:它需要模型“理解”,还是只需要被“执行”?需要模型理解的,才有资格占用Token;能被确定性执行的,一律下沉到外部系统(正如前面提到的,用Hooks、Linter等强制约束)。按这个标准,信息自然分成五层:
| 层级 | 放什么 | 怎么用 | Token 消耗 |
|---|---|---|---|
| 常驻层 | 身份、约定、红线 | 每次必须在场,精简到不能再删 | 最高 |
| 按需加载层 | Skills、领域知识 | 占位符常驻,触发时才展开全文 | 较高 |
| 运行时注入层 | 时间、渠道、用户偏好 | 每轮动态拼入,用完即走 | 中等 |
| 记忆层 | 跨会话沉淀 | 落盘到 MEMORY.md,召回时才读 | 极低 |
| 系统层 | Hooks、代码规则 | 交给外部执行,对模型不可见 | 零 |
越往下,越不依赖模型,也越不消耗上下文资源。
上下文压缩策略
讲到上下文工程,绕不开上下文压缩。常见策略有以下六种,先说最常见的三种:
- 滑动窗口:早期最简单的做法。当上下文超出阈值,直接截断最早的对话轮次,只保留最近的N条消息(系统提示通常会被保留)。逻辑简单,但代价明显——早期的任务约定、决策依据一旦被截掉就永久丢失。短对话勉强够用,长任务场景基本失效。
- LLM Summary:更通用的方案。由一个独立调用的LLM按照预设规则对历史会话进行压缩。规则的核心是区分“不能动的”和“可以丢的”——角色定义、禁令、任务清单及其完成状态不能压缩;一次性的工具执行结果、过时的中间状态可以丢弃。这种方式尤其适合长任务场景,能精准保留决策路径,同时大幅缩减上下文体积。缺点是压缩质量强依赖规则设计和所选模型的能力。
- 工具结果替换:针对大量工具调用的场景,属于工程侧的自动处理机制。核心思路是:在上下文中预留一个固定大小的槽位,专门存放工具调用结果。所有工具共享这个槽位,新结果直接原地替换旧结果,而非追加堆叠。由于大多数工具结果只需短暂参考,无需长期保留,这种方式能让工具调用的上下文占用保持恒定,不随调用次数增长。
这里说一下LLM压缩可能导致的一些问题,最常见的是该保留的未保留,不该保留的反倒保留了。此外,还有标识符保留错误的问题。可以在Agent的约束文档中明确:
(此处为原文章的Compact Instructions示例,完整保留)
这只是一个简单的示例,具体的压缩规则可以按照项目和任务的实际需要自行明确。
上面三种方式最为常见,下面的则相对复杂和底层一些:
- RAG 检索替代注入:思路与上面几种不同,它不是在压缩已有内容,而是从源头控制注入量。原理是:在构建知识库时,先将所有文档切片,通过Embedding模型将每个切片转换为向量并存入向量数据库;每轮对话时,将当前问题同样做Embedding,转换为向量后与知识库中的所有向量做余弦相似度匹配,取相似度最高的若干片段注入上下文。相关的信息进来,无关的从不出现——上下文始终只包含当下真正需要的内容,而不是将整个知识库都塞进去。
- Token 剪枝(Token Pruning):工作在更底层。模型在计算Attention时,不同Token对当前生成的贡献权重差异很大,剪枝机制会识别出那些权重持续偏低、对结果影响微弱的Token,在推理过程中将其丢弃。这种方式对上层应用完全透明,压缩发生在模型内部,但对模型本身有一定要求,并非所有部署环境都支持。
- KV Cache 复用:解决的是另一个维度的问题。模型每次处理Token时都需要计算Key和Value矩阵,对于系统提示、固定前缀等每轮都重复出现的内容,这部分计算完全相同却每次都在重做。KV Cache复用将这些已计算的结果缓存下来直接使用,既节省了计算开销,也间接降低了推理延迟(即:如果当前请求的输入前缀与之前某次请求完全一致,这部分KV就不需要重新计算,直接从缓存读取)。严格来说,它属于推理加速而非内容压缩,但在上下文工程的整体视角下,是不可忽视的效率手段。
其中,KV Cache复用有点意思。基于它,有些做法对LLM外部的开发者而言比较有意义且可控,值得单独拎出来说说。
Prompt Caching
基于KV Cache复用,可以衍生出一种叫做Prompt Caching的方法来优化上下文结构。简单来说,就是依赖KV Cache命中,使每次请求的Prompt前缀完全一致,从而复用缓存。
这意味着在设计Prompt结构时,应将稳定不变的内容(系统提示、固定知识、角色定义)放在前面,而将每轮变化的内容(用户输入、动态信息)放在后面。否则前缀一变,缓存就会失效,白白浪费资源。
回到前面划分的分层结构:常驻层越稳定,前缀命中率就越高,边际成本也就越低。因此,要求常驻层简短且稳定,这不仅是为了节约上下文、保持模型注意力,同时也是为了保证缓存命中率。
这也解释了Skills为何要按需加载。按需注入的内容追加在稳定前缀之后,不会破坏前缀的缓存;而注入的工具定义只要自身足够稳定,同样可以参与缓存复用。反过来看,如果将大量MCP工具定义全部写死在系统提示里,一旦工具集发生变动,整个前缀的缓存就会失效,每次都要重新计算。
这里有一个反直觉的结论:稳定的大系统提示,其实际成本往往低于频繁变动的小提示。原因在于缓存的写入成本只付一次,后续每次调用命中缓存时,读取费用会大幅折扣——以Anthropic Claude为例,缓存读取费用仅为原价的10%。提示词越稳定、调用次数越多,这个收益就越显著。
Skills
什么是 Skills
Skills是上下文工程中非常有效的一种模式,核心思路是:系统提示只保留索引,完整知识按需加载。
将每一项能力定义成一个独立的Skill文件,系统提示里只放一行描述符——告知模型“有这个能力、什么情况下使用”,完整的操作步骤、规则、示例全部放在Skill文件中,触发时才注入上下文。
这样做有三个直接收益:
- 节约上下文:未被触发的Skill,其完整内容永远不占位置。
- 保持缓存稳定:系统提示前缀不随能力数量增长而膨胀,缓存命中率稳定。
- 易于维护:每个Skill独立管理,修改某一项能力不影响其他内容。
Skill 是路由文件,不是功能介绍
编写Skill最容易犯的错误,是把它写成能力介绍:
(此处为原文章的反例,完整保留)
这种写法模型能读懂,但不知道什么时候该用、用了之后该做什么。
Skill文件应该更像一个路由文件:条件清晰、动作明确、不讲废话。模型读到它,应能直接判断“现在该不该激活”以及“激活之后做什么”。一个好的Skill由四个部分构成:
(此处为原文章的标准Skill.md示例,完整保留)
Skill 的分级与体积控制
Skill文件本身也需要控制体积。常驻在系统提示里的描述符应压缩在1-3行以内,只保留触发条件和能力标签,完整内容留在文件里按需加载。
同时,Skills按使用频率分三级管理:
| 级别 | 定义 | 处理方式 |
|---|---|---|
| 常用 Skill | 几乎每次会话都会触发 | 描述符常驻系统提示,完整内容预加载 |
| 按需 Skill | 特定场景才触发 | 描述符常驻系统提示,触发时才注入完整内容 |
| 冷门 Skill | 极少触发 | 描述符也可不常驻,由用户显式召唤或工程侧条件触发 |
目标是:让高频能力随时就位,让低频能力不占位置,两者都不妥协。
在 Agent 中,触发必须由工程侧保证
Skill在Agent中有一个常见误用:把Skill列表告诉模型,然后等它自己想起来用。
这是错误的。模型不会主动翻查自己有哪些Skill。在多轮对话、任务复杂的情况下,早期注入的描述符很容易被后续内容稀释,注意力会漂移。
正确的做法是:每轮用户输入进来之后,工程侧强制扫描一次Skill列表,判断当前输入命中了哪些触发条件,将对应的完整Skill内容注入本轮上下文,再交给模型处理。
用户输入 → 工程侧扫描 Skill 列表 → 命中触发条件 → 注入对应 Skill → 模型处理
触发判断可以用轻量模型做语义匹配,也可以用规则或关键词粗筛。核心原则只有一条:触发这件事不能依赖主模型的记忆,必须由外部机制保证每轮执行。
脚本执行型 Skill:把工具能力内化进上下文
Skills并不局限于纯语言任务。有一类特殊的Skill,其完整内容里直接提供可执行脚本,由Agent在本地环境中运行,从而获得与MCP工具类似的真实执行能力——但不需要接入任何外部服务。
(此处为原文章的Skill示例,完整保留)
这类Skill本质上是将工具能力内化为上下文规程:脚本由Agent在本地执行,结果由模型解读和汇报,整个过程不依赖任何外部服务协议。
Skills 与 MCP 的关系
MCP通过标准协议接入外部工具,让模型可以调用搜索、数据库、代码执行等真实能力。Skills和MCP解决的不是同一个问题,但经常在同一Agent里共存:
| 对比维度 | Skills(含脚本执行型) | MCP 工具 |
|---|---|---|
| 本质 | 上下文中的操作规程 | 外部系统的能力接口 |
| 执行方 | 模型按规程执行 / Agent 本地运行脚本 | 外部工具实际执行,模型调用 |
| 适用场景 | 推理、写作、分析、本地命令、轻量数据处理 | 远程 API、数据库、跨系统集成 |
| 上下文占用 | 描述符极小,按需注入 | 工具定义随接入数量线性增长 |
| 外部依赖 | 无 | 依赖外部服务可用性 |
| 接入成本 | 写一个 Markdown 文件 | 需要部署和维护 MCP Server |
| 安全管控 | 通过 Skill 内约束字段控制执行范围 | 依赖 MCP Server 的权限设计 |
两者不是替代关系,而是分工关系:
- 能用Skills解决的,不引入MCP——简单任务、本地环境、快速接入。
- 需要对接远程服务、跨系统集成的,MCP是更合适的选择。
判断标准只有一个:这件事需要访问外部系统吗?不需要,用Skill;需要,用MCP。但如果被对接的系统配合,也完全可以用curl这类工具,以Skill的方式注入,直接调用对应接口。
文件系统和上下文,就像外存和内存
上述Skill的本质,是以文件系统作为中转层实现按需加载,从而节约上下文空间。这与计算机的存储层级结构高度相似:可以将Agent的上下文窗口类比为RAM——容量有限,但数据必须加载其中才能被处理;而文件系统则对应磁盘(外存)——容量大、可持久化,数据按需读入内存。
这一思路不只适用于Skill,同样适用于Agent的内置Tool。上下文中只需保留核心能力(如读文件、执行脚本),其余工具仅保留名称与功能描述,在实际调用时再动态加载详细定义。这可以大幅降低Token消耗。
将信息持久化到文件系统,同样能带来更强的可靠性。例如,将任务执行进度与各步骤结果记录在progress.md中,一旦任务中断,Agent可以快速完成状态重建、继续执行。在上下文压缩时,也可以将压缩前的内容以结构化形式写入文件系统保存,而非直接丢弃——即便后续真的出现上下文丢失,Agent也能从历史记录中找回关键信息,完成内容补全。
记忆系统的设计
Agent没有原生的记忆
人类的记忆是连续的。昨天发生的事,今天醒来还记得。但大语言模型驱动的Agent不是这样工作的。
每一次会话,对Agent来说都是一次“全新的诞生”。上下文窗口装载了对话内容,推理在其中进行,输出生成后,会话结束——这一切随之清空。下一次启动时,Agent不会记得上一次说了什么、做了什么、用户是谁、任务进行到哪里。
这不是bug,而是Transformer架构的基本特性:模型本身无状态,状态由上下文承载,上下文不持久化,则状态不存在。
因此,要让一个Agent系统具备跨会话的一致性——记住用户偏好、延续任务状态、积累领域知识——记忆层必须单独设计,作为独立的基础设施存在。它不是功能迭代时可以“顺手加上”的能力,而是系统架构的地基。没有记忆层,Agent永远是一个失忆的执行者;有了记忆层,它才能成为一个真正可信赖的协作伙伴。
四种记忆形式
记忆层并非单一结构。根据时效性、结构化程度和更新频率的不同,可以将Agent的记忆拆分为四个层次,各司其职。
1. 上下文窗口(Context Window)
这是Agent的工作记忆,也是唯一天然存在的记忆形式。当前对话的所有内容——用户输入、Agent的回复、工具调用结果、系统提示——都存在于上下文窗口中。模型的每一次推理,都以此为全部的“现实”。
上下文窗口的特点是:即时、高保真,但容量有限且不持久。主流模型的窗口从几万到几十万token不等,对于单次任务绰绰有余,但无法承载跨会话的历史积累。将所有记忆都堆进窗口,既不现实,也会稀释注意力,降低推理质量。
上下文窗口是记忆系统的“前台”,其他层次的记忆,最终都要以适当的方式注入到这里,才能被Agent感知和使用。
2. Skills(技能库)
Skills是Agent的程序性记忆,对应人类“知道怎么做”的那部分认知。它存储的不是事实,而是经过验证的操作流程、工具调用模式、任务解决策略。例如:如何调用某个API、处理某类异常的标准步骤、特定领域的分析框架。
Skills通常以结构化的方式组织,可以是代码片段、函数定义、提示模板或标准操作文档(SOP)。当Agent遇到已知类型的任务时,从Skills库中检索对应的处理方式,而不是每次从零开始推理。
这一层的核心价值在于可复用性和稳定性。Skills一旦验证有效,可以反复调用,降低推理成本,同时减少因模型随机性带来的行为漂移。
3. JSONL 会话历史(Episodic Memory)
JSONL会话历史是Agent的情景记忆,记录“发生过什么”。每一条会话记录以结构化的JSON格式追加写入文件,包含时间戳、用户输入、Agent响应、工具调用链、任务状态等字段。JSONL格式(每行一个JSON对象)天然支持流式写入和增量读取,非常适合作为会话归档格式。
(此处为原文章的JSONL示例,完整保留)
会话历史不直接全量注入上下文——那会超出窗口限制。而是通过检索或摘要的方式,将相关片段召回,按需注入。它是记忆系统的“原始档案”,保真度最高,也是其他记忆层更新的数据来源。
4. MEMORY.md(语义记忆)
MEMORY.md是Agent的语义记忆,是对长期积累信息的结构化摘要与提炼。它以Markdown格式存储,内容可读性强,便于人工审查和编辑。
(此处为原文章的MEMORY.md示例,完整保留)
MEMORY.md在每次会话开始时,作为系统提示的一部分注入上下文窗口,为Agent提供稳定的“世界观”背景。其内容由会话历史定期蒸馏生成,更新频率低但信息密度高。
不同层级记忆之间的协作
四种记忆不是孤立存在的,它们构成一个有机的流转系统。
- 会话启动时:MEMORY.md的内容注入系统提示,为Agent提供基础上下文;相关的历史会话片段通过语义检索召回,补充细节;匹配的Skills也一并加载。
- 会话进行中:所有交互实时写入JSONL历史,上下文窗口动态维护当前对话的完整状态。
- 会话结束后:触发后台异步任务,对本次会话进行摘要分析,更新MEMORY.md中的相关条目;如果本次任务产生了可复用的操作模式,则提炼并写入Skills库。
阈值触发的上下文压缩
长时间运行的会话会遇到一个现实问题:上下文窗口是有上限的。随着对话轮次增加,窗口逐渐被早期的消息填满,新的信息挤不进来,推理质量也开始下降。解决方案不是简单地截断或丢弃,而是设计一套阈值触发的安全归档流程。
第一步,监测窗口占用率。系统持续追踪当前上下文的token占用量。当占用率达到预设阈值(例如窗口容量的70%)时,触发压缩流程,而不是等到窗口溢出再被动处理。留出缓冲空间,是为了确保压缩操作本身还有足够的上下文可以执行。
第二步,识别待归档的消息段。从窗口中选取最早的一批消息——通常是那些对当前任务直接影响已经减弱的早期轮次——标记为“待归档段”。选取策略可以按时间顺序,也可以结合相关性评分,优先移出与当前任务关联度最低的内容。
第三步,对待归档段做Summary。调用模型对这批消息进行摘要,提炼其中的关键信息:涉及的决策、用户表达的偏好、任务状态的变化、重要的工具调用结果等。摘要的颗粒度要足够细,让后续推理不会因为原始细节的缺失而失去关键判断依据。
第四步,将Summary更新至MEMORY.md。摘要内容按类别合并写入MEMORY.md的对应条目,例如新发现的用户偏好追加到偏好区块,任务状态更新覆盖到项目背景区块。MEMORY.md的每次变更通过版本控制留存,不覆盖历史版本。
第五步,原始消息留档JSONL,从活跃窗口中移除。待归档段的原始消息早已实时写入JSONL历史,此时只需将其从上下文窗口的活跃区域中安全移除即可。移除不等于删除——原始的每一轮对话、每一次工具调用的完整记录,依然完整保存在JSONL文件中,随时可以检索和回溯。
第六步,将Summary注入窗口,替代原始内容继续推理。压缩后,窗口中对应位置由一段简洁的摘要文本替代原来的大段原始对话。Agent的推理上下文得到释放,同时关键信息通过摘要形式得以保留,会话可以无缝继续。
这一机制的本质是:用信息密度换取窗口空间,但绝不以牺牲原始数据为代价。窗口中流转的是经过蒸馏的精华,JSONL中保存的是完整的原始事实,两者分工明确,互为补充。
这种分层协作的设计,使得每一层都专注于自身最擅长的事:窗口负责即时推理,历史负责保真归档,MEMORY.md负责长期提炼,Skills负责能力积累。四层之间通过阈值触发、按需召回、定期蒸馏的机制有机连接,构成一个能够随时间持续生长的记忆体系。
记忆的整合回退:不删除,只归档
记忆系统设计中最容易被忽视的一个原则是:不要删除旧记忆,只将其从活跃窗口中移除。这一原则背后有两个核心考量。
其一,信息价值的不确定性。某条看似过时的记忆,可能在未来的某个场景中重新变得关键。用户更改了偏好?旧偏好依然是理解用户变化轨迹的重要参照。任务方向调整了?之前的探索路径可能在新阶段重新被采用。删除意味着永久失去这个可能性。
其二,系统必须可审计、可回退。记忆的更新是一种写操作,写操作必然存在出错的可能——蒸馏逻辑产生了错误摘要、用户信息被错误归类、Skills收录了一个有缺陷的模式。如果没有回退机制,这些错误会悄无声息地污染整个记忆系统,且难以溯源。
具体的工程实践如下:
JSONL历史只追加,不修改。每一条记录一经写入即为不可变。这是天然的审计日志,任何时间点的系统状态都可以通过重放历史来还原。
MEMORY.md采用版本控制。使用Git管理MEMORY.md文件,每次更新产生一个commit。需要回退时,git revert或git checkout到任意历史版本即可。每个版本的变更记录清晰可查。
Skills的变更采用软删除与版本标记。废弃的Skill不直接删除,而是标记为deprecated,并记录废弃时间和原因。新版本的Skill与旧版本共存,支持在需要时显式调用历史版本进行对比验证。
记忆操作本身也写日志。每次MEMORY.md的蒸馏更新、每次Skills的新增或废弃,都在独立的操作日志中留档,记录触发条件、操作内容和执行时间。这使得记忆系统的演化过程本身变得透明和可追踪。
Agent的记忆层,本质上是在一个无状态的推理引擎之上,用工程手段构建出时间连续性的幻觉——但这不是欺骗,而是让AI系统真正走向实用的必要设计。上下文窗口、Skills、JSONL会话历史、MEMORY.md,四种记忆模式各有其位,协同工作。而“只归档、不删除、全程可回退”的操作原则,则是这套系统能够长期可靠运行的基本保障。记忆层不是Agent的附加功能,它是Agent得以存在于时间之中的方式。
约束与自由,Free Agent!
说到这个,可能会想到LangChain的演化路径:LangChain走过了一条清晰的轨迹——从强约束的Chain管道,到完全放开的AgentExecutor,再到LangGraph用图结构重新引入可控边界。这条路上所说的“自由”,是执行路径的自由——模型能不能自己决定下一步调用哪个工具,看起来就是从约束到自由,再到有限的自由。但Agent工程里还有另一个维度的自由,两者不是同一回事。
这里讲的自主度,核心不是人工介入的次数减少了多少,而是Agent有没有能力在更长的时间跨度里把一件事稳定做完。能否独立完成一个横跨多个session、夹杂文件读写与外部服务调用的复杂任务,决定性因素不是模型的推理天花板有多高,而是配套的基础设施是否到位。
出发点也不是直接将控制权交出去,而是先把两件事做扎实:跨session的续跑能力和单个session内的进度约束机制。这两块没打好地基,Agent的自主度越高,跑偏的风险反而越大。
长任务的现场保存与恢复
长任务失败的真正原因,大多数时候不是某个步骤执行出错,而是session结束但事情还没做完。
哪怕已经开启了上下文压缩,照样有两类问题绕不过去:其一,试图在单个session里把整个应用一口气做完,上下文先到极限,任务被迫中断;其二,只推进了一部分,下一轮session启动时找不回现场,要么把做过的事重来一遍,要么错误地认为任务已经结束。这两种失败的根源一致——任务状态没有被写到模型之外的地方。
一个更可靠的组织方式,是把长任务分给两个各司其职的角色:初始化Agent与Coding Agent。这套分工对代码生成、应用搭建、重构迁移一类的场景尤其合适——任务规模超出单个session的承载边界,但又能切分成一批有明确验收标准的子任务。
初始化Agent仅在整个任务的第一轮出现,它不负责写代码,它负责把任务物化成文件系统里的持久状态:输出结构化的feature-list.json、可执行的初始化脚本init.sh、初始git commit,以及记录当前进度位置的claude-progress.txt。从这一刻起,任务的全貌和起点就落在磁盘上了,跟模型上下文里还剩多少空间无关。
接下来的多轮session交给Coding Agent循环处理。每次进来,先读claude-progress.txt和git log,把现场还原,找到当前该推进的功能,实现它,跑测试,把对应条目的passes字段翻成true,提交,退出。下一轮同样的入口,找下一个未完成项,接着来。哪怕中途崩了,重新拉起来也是从文件系统的状态接着走,不存在“从头再来”的情况。
任务状态必须写出来,不能只活在上下文里
上面提示有两个细节值得单独拎出来说:进度记在文件里,不记在上下文里;功能清单用JSON而不是Markdown。结构化格式在模型读写时更稳定,不容易在格式层面翻车。判断任务是否完成的依据,是feature-list.json里所有条目的passes是否全部为true——是文件说了算,不是模型自己觉得差不多了。
跨session的问题解决的是“下次从哪里接”,但单个session内部照样会出问题。随着任务拉长,上下文里没有外部进度锚点,Agent很容易跑偏——在某个子任务上兜圈子,或者任务明明没做完却提前收工。这不是推理能力的问题,而是工作记忆天然不可靠,上下文越长,早期写进去的任务状态就越容易被稀释掉。
解决办法是把任务状态拿出来作为显式的外部控制对象:
(此处为原文章的JSON任务状态示例,完整保留)
规则本身并不复杂:任意时刻只允许一个in_progress,每推进完一步,先把状态文件改掉,再往下走。当前走到哪一步,不靠模型记住,靠文件记录,每轮推理前读取一次。
可以叠加一层轻量干预:当连续多轮都没有更新任务状态时,自动往上下文里塞一条,点出当前进度和剩余项。逻辑不复杂,但它把“Agent有没有跑偏”从一个只能事后发现的隐患,变成了实时可见、随时可介入的可观测状态。
把前两块地基打好之后,还有一个容易被低估的摩擦点:I/O的组织方式。文件操作、网络请求、耗时较长的外部命令这类I/O操作一旦同步挂在主循环上,整个系统就陷入“模型在干等,token在消耗,任务没动静”的僵局。可以这么干:把这些操作扔到后台线程去跑,结果通过通知队列在下一次LLM调用前注入。主循环不需要操心并发怎么管,只要在每轮开头扫一眼队列有没有新结果,再定下来是继续执行、原地等待还是调整方向。
这个方案的价值不在于技术上有多精妙,而在于它经得起长期维护。如果把整个主循环改造成完整的支持async-await的循环,引入的复杂性往往比它解决的问题还多。后台线程加通知队列,思路直接,出了问题也容易找到根源。对于要长期稳定跑下去的Agent系统,可维护性本身就是竞争力的一部分。
Multi-Agent
单个Agent有其天然的边界:上下文窗口有限、工具集不能无限扩张、长任务容易跑偏。当任务复杂度超出单个Agent的承载能力时,答案不是把这个Agent做得更大,而是引入多个Agent协同工作——这是Multi-Agent的出发点。Multi-Agent是一个总体范式,描述的是“多个Agent参与同一个任务”这件事本身,至于这些Agent如何组织、有没有层级、谁来调度,Multi-Agent并不规定。
常见的组织模式有两种。
一是指挥者模式(同步协作):主Agent充当实时指挥,向Sub-Agent下发指令后等待结果返回,再根据结果决定下一步动作。整个执行过程是同步推进的,主Agent全程掌控节奏,适合任务之间依赖关系紧密、需要根据中间结果动态调整方向的场景。
二是统筹者模式(异步委派):主Agent在任务开始时完成全局规划,将拆解好的子任务批量委派给多个Sub-Agent并行执行,自身退出等待状态,Sub-Agent完成后将结果写入共享存储,主Agent在合适的时机统一收集结果进行整合。适合子任务之间相互独立、可以并行推进的场景。
在实际的工程实现中,统筹者模式通常这样组织:主Agent统筹全局,多个独立的Sub-Agent并行工作;Agent之间通过JSONL消息队列通信,每条消息格式结构化、边界清晰;.worktrees/目录为每个Sub-Agent隔离独立的文件操作空间,避免并行写入互相污染;.tasks/目录维护任务图,记录子任务的依赖关系和当前状态,主Agent通过任务图而非上下文感知整体进度。
隔离与协作:先有协议,再谈并行
Sub-Agent适合承接的,是边界清晰、可独立验收的子任务。代码模块的实现、特定文件的分析、单一服务的调试——这类任务输入输出明确,执行过程不需要主Agent实时介入。Sub-Agent的操作过程、中间状态、调试细节,全部留在自己的独立上下文里,不往主Agent的上下文里渗透。主Agent只关注结果:任务完成了吗?结果是什么?有没有需要上报的异常。过程细节对主Agent是不透明的,也应该是不透明的。
协作方式必须以协议的形式固定下来。模型记不住谁在负责什么、依赖谁的结果、什么时候可以开始下一步。一旦任务之间产生依赖,这些关系就必须从“模型应该知道”变成“协议明确写明”。协议的形式可以是这样:
(此处为原文章的协议JSON示例,完整保留)
任务ID、执行者、依赖项、输入来源、输出路径、验收标准、返回格式——这些都是协议字段,不是自然语言描述,不依赖任何Agent去“理解”或“记住”。
启动顺序不能反。协议先定,隔离先做,再谈协作和并行。具体来说:先用.tasks/将任务图和依赖关系写清楚,再用.worktrees/为每个Sub-Agent划定文件操作边界,最后才是主Agent通过JSONL消息队列分派任务、Sub-Agent执行后只回复摘要。顺序一旦颠倒,隔离没做好就开始并行,出了问题既不知道是谁改的,也不知道改了什么。
幻觉在多 Agent 之间会被放大
单个Agent产生幻觉,影响是局部的。多个Agent频繁交互时,幻觉会被逐层放大:Agent A输出了一个带偏差的结论,Agent B将其作为可信输入继续推理并进一步强化,Agent C在此基础上叠加,最终所有Agent收敛到同一个高置信度的错误结论。每个Agent单独看都“言之凿凿”,但整个系统已经跑偏了。
应对这个问题,需要引入独立的交叉验证机制。最直接的方式是设置一个游离于主执行链之外的验证Agent,它不参与任务执行,只负责对其他Agent的关键输出进行独立审查——用不同的推理路径、不同的工具、甚至不同的模型,得出独立判断,再与主链结论对照。除此之外,也可以采用多路并行再投票的方式:对同一个关键子任务,派发给两个独立的Sub-Agent分别执行,结果一致才采信,不一致则上报主Agent裁决。
核心原则是:关键结论不能只有一个信源,系统内部要有能独立说“不”的声音。
控制 Sub-Agent 的数量与深度
Multi-Agent系统很容易在“再加一个Agent来处理这个问题”的路上越走越远,最终变成一张没人看得清楚的调用网。
Sub-Agent的数量和调用深度都需要硬性约束。调用层级超过三层,整个系统的可观测性就会急剧下降——主Agent不知道第三层的Agent在做什么,出了问题也很难溯源。并行Sub-Agent的数量同样要有上限,不是越多越快,协调开销和状态同步的复杂度会随数量非线性增长。
系统提示也要最小化。给每个Sub-Agent的系统提示,只包含它完成当前任务所必需的信息:角色定义、工具权限、输出格式要求、返回规范。背景知识、全局目标、其他Agent的情况——一概不给。系统提示越长,Agent的注意力就越分散,执行的稳定性就越差。给得少,反而做得准。
如何评测一个Agent
评测的本质与核心要素
要评测一个Agent的好坏,本质上需要三样东西:测试用例、评测标准、自动验证机制。
但在这三者之间,有一个容易被忽视的陷阱——分数高,效果就一定好吗?答案肯定是不一定。评测本身是有盲区的。测试用例的覆盖范围、评分器的设计质量,都会直接影响最终的分数是否“可信”。一个在测试集上表现优秀的Agent,放到真实业务场景里可能一塌糊涂。所以评测的首要目标,不是追求高分,而是让这个分数有意义、可信赖。
传统NLP评测相对可控:给一个输入,期望一个输出,差距可量化。但Agent的评测面临两个根本性挑战:
第一,输入空间几乎是无限的。Agent面对的是开放世界——用户的意图千变万化,工具调用的组合方式也是指数级的。不可能用有限的测试用例覆盖所有场景。
第二,同一任务多次执行,结果也会有差异。由于大模型的随机性(temperature > 0),加上工具调用链路的复杂性,同一个Agent对同一个问题,第一次可能答对,第二次可能就答错了。这种“不稳定性”是评测中必须正视的问题。
两个关键指标:pass@k 与 pass^k
正是为了应对上述不稳定性,Agent评测引入了两个互补的指标:
pass@k —— 探索能力上限
对同一任务运行k次,只要有至少一次答对,就算通过。这个指标衡量的是Agent的能力天花板:它在理论上能否解决这类问题。如果pass@k很低,说明Agent根本不具备处理该任务的能力,改模型、改架构是当务之急。
pass^k —— 基础质量保证
同样运行k次,要求每一次都答对,才算通过。这个指标衡量的是Agent的稳定性和可靠性:它能否持续稳定地完成任务。在生产环境中,这个指标往往比pass@k更重要——用户不会给10次机会。
三类评分器:如何判断答案对不对?
知道了怎么跑,还要知道怎么“判”。Agent的输出形式多样,不同类型的输出适合不同的评分方式。
1. 代码评分器 —— 有明确答案时首选
适用场景:输出有确定性答案的任务。比如数学计算、SQL查询结果、代码运行输出、结构化数据提取等。做法:写脚本进行精确匹配或规则校验,比如比对数值是否相等、JSON结构是否正确、代码执行结果是否符合预期。确定性最高,结果非黑即白,没有歧义,可完全自动化,速度快,成本低。
2. 模型评分器(LLM-as-Judge)—— 评价语义质量
适用场景:输出需要主观判断的任务。比如文本摘要质量、回答是否满足用户意图、对话是否连贯等。做法:用一个强大的LLM作为裁判,给它设计清晰的评分Prompt,让它判断Agent的输出是否满足要求,并给出评分或理由。确定性中等。模型评分器本身也有随机性,同样可能判断不稳定,需要多次采样取均值,或设计一致性校验。注意:Prompt的设计质量直接决定评分质量,要避免让裁判模型“放水”或“偏心”。
3. 人工评分器 —— 兜底的最终裁判
适用场景:代码评分器和模型评分器都拿不准的情况。比如输出涉及价值判断、专业领域知识、或者评分结果存在分歧时。做法:由人工专家对输出进行标注和评分,必要时引入多位标注者取众数,计算标注一致性(Cohen's Kappa)以保证评分可靠。确定性最低,但质量最高。成本高、速度慢,适合用于校准其他评分器,或处理高价值的关键案例。
三类评分器的层级使用策略
实践中,这三类评分器不是非此即彼,而是按层级叠加使用:代码评分器(优先)→ 模型评分器(次之)→ 人工评分器(兜底)。能用代码判的,绝不用模型判;模型判不了的,才交给人工。这样既保证了效率,又保证了准确性。
如何搭建一个可靠的评测环境?
理论说完了,落地时还有几个关键工程细节:
用例选择要有共识
测试用例必须选择人与人之间没有异议的,标准清晰、预期确定。模糊的、主观性强的用例,评测结果本身就不可信,不要放进核心测试集。同时,测试用例要同时包含正例和反例。正例验证Agent该做的事做到了,反例验证Agent不该做的事没有做——两者缺一不可。
测试环境要隔离
不同轮次的测评之间,必须保证环境隔离。上一轮的执行结果、缓存、状态,不能污染下一轮。否则看到的“稳定性提升”,可能只是因为命中了缓存。
结果不好时,先别急着改 Agent
这是一个反直觉但非常重要的原则:
当评测结果很差时,第一步不是去改Agent,而是先审查测试用例本身有没有问题。
实际工作中,很多“Agent表现差”的根源,是测试用例写错了、预期答案有歧义、或者评分器设计有bug。如果没搞清楚这一点就急着调整Agent,最终可能越改越偏。
正确的顺序是:
- 审查测试用例——预期答案是否正确?用例覆盖是否合理?
- 审查评分器——评分逻辑是否准确?是否存在系统性偏差?
- 确认是Agent问题后——再针对性地优化Agent。
如何跟踪Agent的执行过程
为什么 Agent 需要 Trace 体系
做过后端开发的人都知道,每一个API请求都会携带一个trace_id。当线上出现问题时,拿着这个ID就能把整条请求链路完整地还原出来——哪个服务出了问题,哪一步耗时异常,一目了然。
Agent同样需要这样一套机制,而且需求更加迫切。原因在于:Agent的错误和传统代码的错误有本质区别。传统代码的bug通常是确定性的——同样的输入,必然触发同样的错误,复现容易。但Agent的错误,更多发生在某一轮的决策层面:它选错了工具、误解了用户意图、在多步推理的第三步走偏了方向。这类错误往往难以复现,事后也很难说清楚“它当时到底在想什么”。
没有完整的记录,就没办法稳定地复现失败案例;没办法复现,就没办法定向修复。Trace体系,是Agent工程化的基础设施,不是可选项。
Trace 里应该记录什么?
每一次Agent的完整运行,都应该留下一份详尽的“执行档案”。具体来说,至少需要包含以下内容:
- 完整的Prompt内容:不只是用户输入的那句话,而是实际送进模型的完整Prompt——包括System Prompt、动态注入的技能描述(Skills)、上下文背景信息等。很多时候问题就出在这里:注入的内容有误,或者拼接逻辑出了问题,但如果只记录“用户说了什么”,根本看不出来。
- 完整的对话历史:用户与Agent之间每一轮的消息记录,按时序完整保存。多轮对话中,早期轮次的信息往往会影响后续决策,缺了任何一轮都可能导致溯源断链。
- 每一次工具调用的入参和返回值:Agent调用工
