Agent 框架核心设计
设计一个 Agent 框架是一个有深度的系统工程问题。本文尝试从架构层面系统梳理核心设计要素——不是罗列概念,而是把每个子系统的设计决策、工程权衡和演进方向讲清楚。
一、核心循环(Agent Loop)
一个 Agent 框架本质上要解决的问题是:让 LLM 能够感知环境、做出决策、执行动作,并根据反馈迭代,直到完成目标。最基础的抽象是一个感知-思考-行动的循环:
while not done:
observation = perceive(environment)
thought = reason(observation, memory, goal)
action = decide(thought, available_tools)
result = execute(action)
memory.update(result)
done = check_termination(result, goal)
这六行代码背后隐藏着大量设计决策。
终止条件是第一个需要确定的问题。常见策略包括硬性最大步数限制(防止无限循环烧钱)、目标达成检测(需要定义"达成"的判定标准)、LLM 自判断(模型输出特定 token 表示任务完成)。实践中通常是多条件组合——模型自判断为主,最大步数兜底,关键操作前插入人类确认。
单步粒度决定了循环的"节奏"。粒度太细(每个 API 调用都是一步)导致推理调用次数爆炸;粒度太粗(一次规划多步执行)则失去了中间纠错的机会。Claude Code 的做法值得参考:默认让模型自由决定单步包含多少动作,但在高风险操作(写文件、执行命令)前强制插入确认点。
失败回退是循环鲁棒性的关键。最简单的是重试(相同输入再跑一次,利用模型输出的随机性),进阶是 re-prompting(把错误信息追加到上下文让模型自我修正),最复杂是 checkpoint-rollback(回退到上一个已知正确状态重新规划)。Devin 在编程任务中的 checkpoint 机制本质上就是把 Agent Loop 从线性链变成了可回溯的有向图。
循环架构还有一个容易被忽视的维度:同步 vs 异步。同步循环简单直观,但遇到长时间工具执行(编译、部署、等待审批)时会阻塞整个 Agent。异步事件驱动架构允许 Agent 在等待期间处理其他任务或接收新的外部信号,代价是状态管理复杂度显著上升。Google 的 A2A 协议中 Task 的状态机设计(submitted → working → input-required → completed/failed)就是为异步场景准备的。
二、工具系统(Tool / Action Space)
工具系统是 Agent 与外界交互的接口层,也是 Agent 从"能说"到"能做"的关键跃迁。
2.1 工具描述协议
每个工具需要一个 schema,包含名称、功能描述、参数定义(类型、是否必填、语义说明)、返回值格式。这本质上是给 LLM 提供"API 文档",描述的质量直接决定工具调用的准确率。
一个反直觉的经验:工具描述中"什么时候不该用这个工具"往往比"什么时候该用"更重要。模型在面对模糊意图时倾向于调用看起来"沾边"的工具,明确的负面边界能显著降低误调用率。
工具描述的另一个工程细节是返回值的信息密度。工具返回过多原始数据会撑爆上下文窗口,返回过少又让模型无法做出下一步判断。好的实践是让工具返回"摘要 + 关键细节 + 获取更多信息的方式"——类似 Unix 哲学中 ls 默认简洁输出但支持 -l 详细模式。
2.2 工具注册与发现
静态注册(启动时加载全部工具定义到 system prompt)在工具数量少于 15-20 个时工作良好。超过这个阈值后,工具定义本身开始消耗模型的注意力预算,调用准确率反而下降。
动态发现的解法有几种层次:
第一层是分组路由——按领域将工具分组(文件操作、网络请求、数据库查询),先让模型选组再在组内选工具。这相当于把 O(n) 的选择问题变成 O(log n)。
第二层是语义检索——将工具描述向量化,根据当前任务语义检索最相关的 top-k 工具注入上下文。MCP 协议的 Resources 机制天然支持这种按需加载模式。
第三层是工具生成——当现有工具都不满足需求时,Agent 自行编写新工具代码并注册。这在 Voyager(Minecraft Agent)中被验证有效,Agent 会把成功的工具代码存入"技能库"供后续复用。这种"元工具"能力模糊了工具使用者和工具创造者的边界。
2.3 执行隔离
工具执行是否需要隔离环境取决于两个因素:工具的破坏力(只读查询 vs 写入操作 vs 系统命令)和运行环境的信任等级(本地开发 vs 多租户服务 vs 面向公网)。
隔离方案从轻到重:应用层权限检查(零开销但防不住恶意构造)→ OS 沙箱如 macOS Seatbelt / Linux seccomp(毫秒级开销,阻断系统调用)→ 容器 + gVisor 用户态内核(百毫秒级,拦截内核交互)→ Firecracker MicroVM(独立内核实例,~350ms 启动,最强隔离)→ WebAssembly(编译时约束 + 微秒级启动,适合高频创建销毁)。
选型的核心权衡是启动延迟 vs 隔离强度。编程 Agent 每次执行代码都要创建沙箱,如果启动耗时超过 1 秒,用户体验会显著恶化。Firecracker 的 snapshot 快速恢复和 WebAssembly 的即时启动都是在这个约束下的工程优化。
三、记忆系统(Memory)
记忆系统解决的核心问题是:如何让 Agent 在有限的上下文窗口约束下,表现得像拥有无限记忆。
3.1 工作记忆(Working Memory)
就是当前对话的上下文窗口。核心挑战是 context 管理——当对话超长时如何压缩、摘要、或选择性遗忘。
常见策略包括滑动窗口(丢弃最早的消息)、关键信息摘要(用小模型将历史压缩为结构化摘要)、以及按相关性检索历史片段(不按时间顺序,而是按与当前问题的语义相关性选择保留哪些历史)。
一个重要的设计决策是摘要的触发时机和粒度。过早摘要会丢失后续可能需要的细节,过晚摘要则上下文已经溢出。Anthropic 的做法是设置 token 阈值(比如 80% 窗口容量),触发后对最早的 N 轮对话执行摘要压缩,保留最近的对话原文。摘要本身也是一种有损压缩——什么信息值得保留、什么可以丢弃,这个判断本身就需要对任务目标的理解。
3.2 长期记忆(Long-term Memory)
用于跨会话持久化。通常用向量数据库存储过往经验、用户偏好、事实知识,检索时按语义相似度召回。
设计时要注意两个容易被忽视的问题:
时效性——记忆不是存了就永远有效。用户偏好会变、项目架构会重构、技术栈会迁移。需要设计衰减机制(越久远的记忆权重越低)或显式失效(用户纠正时标记旧记忆为过期)。Mem0 的做法是每次写入新记忆时检查是否与已有记忆冲突,冲突时用新的覆盖旧的。
检索精度 vs 召回率的权衡——注入太多"可能相关"的记忆会稀释上下文注意力,注入太少又可能遗漏关键信息。实践中通常设置 top-k 上限(5-10 条)并要求相似度超过阈值,宁可少召回也不要引入噪声。
3.3 程序性记忆(Procedural Memory)
这是 Agent 学到的"做事方式"——成功完成某类任务的步骤模板、踩过的坑、常见错误的修复方法。
实现方式从简单到复杂:few-shot examples(把成功案例直接放进 prompt)→ 结构化 SOP 文档(按任务类型索引的操作手册)→ 可执行的工作流模板(Agent 识别任务类型后加载对应的执行框架)。
CLAUDE.md / AGENTS.md 这类"项目记忆文件"本质上就是程序性记忆的最简实现——完全透明、可版本控制、可人工编辑。它的优势在于避免了向量数据库的"记忆黑箱"问题:用户能精确知道 Agent 记住了什么,也能直接修改。缺点是不能自动积累,需要人工维护。
更进一步的方向是自动化程序性记忆提取:Agent 完成任务后自动总结"这次做对了什么、踩了什么坑",结构化存储后在遇到相似任务时自动召回。Voyager 的 skill library 和 Reflexion 的 experience buffer 都是这个思路的具体实现。
四、规划与推理(Planning)
简单任务可以靠 ReAct(Reasoning + Acting 交替)逐步推进,但复杂任务需要更结构化的规划能力。
4.1 任务分解
将高层目标拆成子任务树。两种基本策略:
Plan-then-Execute——先生成完整计划再逐步执行。优点是执行阶段可以并行、可以分配给不同的专家 Agent;缺点是计划基于初始信息制定,执行中发现新情况时调整成本高。适合任务结构相对确定的场景(比如"把这个 Python 2 项目迁移到 Python 3")。
Adaptive Planning——边执行边调整计划。每完成一步都重新评估剩余计划是否仍然合理。鲁棒性强但 token 消耗大(每步都要重新推理整体计划)。适合探索性任务(比如"调查这个性能问题的根因")。
实践中的最优解通常是混合:先做粗粒度规划确定大方向,执行中在每个子任务内部用 adaptive 方式灵活应对。类似军事中"战略层面计划、战术层面随机应变"的思路。
4.2 反思与纠错
执行结果不符合预期时,Agent 需要能诊断原因并调整策略。Reflexion 模式是一个典型范式:执行 → 评估 → 生成反思 → 修正计划 → 重新执行。
反思的质量取决于两个因素:评估信号的丰富度(仅知道"失败了"远不如知道"在第 3 步调用 API 时返回 403,原因是缺少认证 header"有用)和反思的结构化程度(自由文本反思容易流于表面,结构化模板——"发生了什么 / 为什么 / 下次怎么避免"——能引导更深入的分析)。
一个工程上的关键决策是反思的存储和复用。如果反思只存在于当前会话的上下文中,Agent 下次遇到相同问题还会重蹈覆辙。将反思结构化存入程序性记忆,按错误模式索引,才能实现真正的"从错误中学习"。这与人类的专家成长路径一致——专家之所以是专家,不是因为不犯错,而是因为同样的错误不犯第二次。
4.3 多路径探索
对于不确定性高的任务,可以并行探索多条路径。Tree of Thoughts 将推理空间从线性链拓展为树形结构:每个决策点生成多个候选思路,评估各路径的潜力,选择最优或回溯尝试其他分支。
工程实现的核心挑战是评估函数的设计——如何在路径尚未完成时判断其"潜力"。常见方案包括:让模型自评(快但可能过于乐观)、用独立的评估模型打分(更客观但增加延迟)、用启发式规则快速过滤明显不可行的路径(比如已经违反约束条件的分支直接剪枝)。
计算成本是多路径探索的主要限制。如果每个决策点展开 3 条路径、任务需要 5 步决策,总推理次数是 3^5 = 243 次——这在延迟和成本上通常不可接受。实际应用中需要激进的剪枝策略,或者只在关键决策点(高不确定性 + 高影响)才展开多路径。
五、上下文工程(Context Engineering)
上下文工程是 2025 年逐渐从 Prompt Engineering 中分化出来的独立学科。它关注的不是"怎么措辞",而是"让模型在做决策时看到什么信息"。
5.1 上下文腐烂
即便模型支持 200K tokens 的窗口,也不能无节制填充。实验表明模型对上下文中间位置的信息关注度显著低于首尾("Lost in the Middle" 现象),且随着总 token 数增加,对任意单条信息的推理精度都在下降。
这意味着上下文管理不是"能放多少放多少",而是一个注意力预算分配问题:每引入一个 token 都在消耗有限的注意力资源,必须确保每个 token 对当前决策有正向贡献。
5.2 四大管理策略
写入(Write)——决定什么信息值得记录到上下文中。不是所有工具返回结果都需要完整保留,不是所有对话历史都同等重要。关键是识别决策点、错误转折和任务进度标记。一个反直觉的经验:删除失败尝试的记录会导致 Agent 重复犯错,正确做法是保留失败事实 + 原因分析。
选择(Select)——从所有可用信息中筛出与当前步骤最相关的子集。核心原则是质量大于数量——5 条高度相关的信息优于 50 条"可能有用"的信息。实现手段包括语义检索、规则过滤、以及让模型自己判断"我现在需要什么信息"。
压缩(Compress)——在保留核心语义的前提下减少 token 消耗。手段包括对话历史摘要、代码块精简(只保留签名和关键逻辑,省略样板代码)、冗余信息合并。压缩是有损的,设计时需要明确"什么信息绝对不能丢"。
隔离(Isolate)——用子 Agent 作为"上下文防火墙"。当任务涉及多个不相关子问题时,为每个子问题启动独立 Agent 会话,各自维护干净的上下文,最后只将结论汇总回主 Agent。这避免了不同子任务的中间状态相互污染。
5.3 分层上下文架构
按信息的变化频率分层,变化越慢的越靠前(越适合 KV-cache 复用):
L1 系统指令 | 角色定义、安全约束、输出格式要求 | ~5K tokens | 完全静态,跨会话复用 |
L2 工具定义 | 当前可用工具的 JSON Schema | ~10K tokens | 会话内稳定,偶尔动态增减 |
L3 项目上下文 | 项目结构、技术栈、编码规范、记忆召回 | 2-20K tokens | 按任务动态组装 |
L4 对话历史 | 用户消息、模型回复、工具调用及结果 | 持续增长 | 不可缓存,需主动管理 |
这种分层的工程价值在于:L1-L2 层可以利用 Prompt Caching 技术(Anthropic 和 OpenAI 都已支持)将首次计算的 KV-cache 复用到后续请求,显著降低延迟和成本。设计上下文时有意识地把稳定内容前置、动态内容后置,能最大化缓存命中率。
六、多 Agent 协作
当单个 Agent 的能力或上下文不足以覆盖全部任务时,需要多 Agent 架构。多 Agent 不是"更高级"的架构——它是解决特定问题(专业化分工、上下文隔离、并行加速)的工程手段,引入它的同时也引入了通信开销和协调复杂度。
6.1 编排模式
中心化调度(Orchestrator → Specialists):一个编排 Agent 理解全局任务、制定计划、将子任务分发给专家 Agent、收集结果做最终决策。优点是全局可控、流程可预测;缺点是编排 Agent 本身可能成为瓶颈(它需要理解所有子任务的输入输出格式),且单点故障风险。
去中心化协商(Peer-to-Peer):每个 Agent 处理自己的任务时,发现缺少信息或能力就主动发起对其他 Agent 的查询或委托。灵活性高,适合任务边界模糊的场景;但协调成本大,容易出现循环委托或信息不一致。
层级式(Manager → Team Lead → Worker):多层树状结构,每层只与相邻层通信。适合大规模任务分解,但层级过深会导致信息传递失真(类似大公司的"传话游戏")。
实际生产系统通常是混合模式:顶层用中心化编排保证整体方向可控,子任务内部允许 Agent 之间对等协商处理细节。
6.2 通信协议
Agent 之间的通信需要解决几个问题:
任务委托的语义——委托时传什么上下文(全部历史 vs 精简摘要 vs 只传目标描述)、结果怎么回传(原始输出 vs 结构化摘要)、失败怎么升级(重试 vs 换一个 Agent vs 上报人类)。
不透明性原则——调用方不需要知道被调用 Agent 的内部实现(用什么模型、什么框架、什么记忆系统)。这与微服务架构中的封装原则一致,允许各 Agent 独立演进。Google 的 A2A 协议将此作为核心设计决策:Agent Card 只暴露能力描述和接口格式,不暴露实现细节。
通信模式——同步请求/响应(简单查询)、流式传输(长任务增量推送进度)、异步回调(完全解耦,适合跨系统协作)。选择取决于任务的时间特性和系统的耦合度要求。
6.3 状态共享
各 Agent 之间是否共享记忆,还是各自维护局部状态再在需要时同步?
完全共享(所有 Agent 读写同一个记忆存储):协作最紧密,但容易出现写冲突和 context 膨胀——一个 Agent 写入的大量中间状态可能对其他 Agent 完全无用却占据了它们的注意力。
完全隔离(每个 Agent 独立记忆,只通过消息传递信息):最干净,但信息同步成本高,容易出现各 Agent 对同一事实的理解不一致。
分层共享是更实用的折中:全局共享一个只读的"事实层"(项目结构、技术栈、已确认的决策),每个 Agent 维护自己的"工作层"(当前子任务的中间状态),任务完成后将结论提交到事实层。这类似数据库的"读写分离 + 最终一致性"模型。
七、安全与控制
Agent 安全与传统软件安全有本质区别:传统软件的行为是确定性的(相同输入相同输出),Agent 的行为是概率性的——模型可能产生任何动作,包括开发者未预见的危险操作。安全设计必须假设"Agent 会做出意料之外的事"。
7.1 权限分级
不同工具的风险等级不同,需要分级管控:
低风险(读取类操作:查文件、搜索、查询数据库)——自动执行,无需确认。
中风险(写入类操作:修改文件、发送消息、创建资源)——根据配置决定是否需要人类确认。可以设置"信任阈值":Agent 在当前会话中连续成功执行 N 次同类操作后自动提升信任等级。
高风险(不可逆操作:删除数据、执行支付、修改权限)——强制人类确认,且确认时展示完整的操作详情和影响范围。
权限模型的设计哲学是最小权限原则:Agent 默认只拥有完成当前任务所需的最小权限集,需要额外权限时显式申请。这与 Unix 的 sudo 机制和移动端的运行时权限申请是同一思路。
7.2 护栏(Guardrails)
输入护栏——防止 prompt injection。用户输入或工具返回的内容可能包含恶意指令试图劫持 Agent 行为。防御手段包括:输入内容与系统指令的明确分隔标记、对用户输入做意图分类(正常请求 vs 注入尝试)、关键操作前的二次确认。
输出护栏——防止信息泄露和格式错误。Agent 的输出可能无意中包含敏感信息(API key、内部路径、用户隐私数据)。需要在输出层做正则匹配和脱敏处理。
行为护栏——防止 Agent 进入危险的行为模式。比如检测到 Agent 在循环重试同一个失败操作(可能在烧钱)、或者在尝试访问明显超出任务范围的资源(可能被注入攻击)时,自动中断并报警。
7.3 可观测性
每一步的 thought、action、observation 都需要可追溯,方便调试和审计。这不只是 logging,还包括结构化的 trace——类似分布式系统的 OpenTelemetry tracing,每个 Agent 调用是一个 span,工具执行是子 span,多 Agent 协作形成完整的 trace 树。
可观测性的三个维度:
决策过程——模型为什么做出这个选择?需要记录模型的 reasoning(思维链)、当时的上下文快照、以及候选动作的排序。
执行轨迹——哪些工具被调用、参数是什么、返回了什么、耗时多少。这是最基础的审计日志。
资源消耗——每步的 token 使用量、API 调用成本、端到端延迟。这些指标对成本控制和性能优化至关重要。
八、实际工程考量
8.1 延迟与成本
每次 LLM 调用都有时间和金钱成本。一个典型的 Agent 任务可能需要 10-50 次模型调用,如果每次 2-5 秒,总延迟就是 20-250 秒。优化手段:
模型分层——路由层用轻量模型(GPT-4o-mini、Haiku)做意图识别和简单判断,复杂推理才调用大模型(GPT-4o、Opus)。这能将平均单次调用成本降低 5-10 倍。
并行化——当多个工具调用之间没有依赖关系时并行执行。比如同时读取 5 个文件,而不是串行读取。
缓存——相同的系统提示 + 工具定义可以利用 Prompt Caching 避免重复计算 KV-cache。对于确定性工具(相同输入必然相同输出),可以缓存工具执行结果。
投机执行——在模型还在生成时就预测可能的工具调用并提前准备执行环境(预热容器、建立连接)。
8.2 错误恢复
网络超时、工具执行失败、LLM 输出格式错误——这些都是高频问题,框架必须优雅处理。
格式修复(Format Recovery)——模型输出不符合预期格式时(比如 JSON 解析失败),将错误信息追加到上下文要求模型重新生成。通常 1-2 次重试就能修复。关键是错误提示要具体——"你的 JSON 在第 15 个字符处缺少闭合括号"比"格式错误请重试"有效得多。
工具执行失败——区分瞬时错误(网络抖动,重试即可)和持久错误(权限不足、资源不存在,重试无意义)。对持久错误,需要将失败原因反馈给模型让它调整策略(换一个工具、换一种方法、或者向用户求助)。
优雅降级——当某个能力不可用时(比如代码执行沙箱挂了),Agent 应该能识别这个限制并调整行为(比如改为只生成代码不执行,让用户自行验证),而不是反复重试直到超时。
8.3 评测与迭代
Agent 的效果很难用单一指标衡量。需要多维度评测体系:
任务成功率——在标准 benchmark 上的通过率。但要注意 benchmark 的代表性——SWE-bench 衡量的是代码修复能力,不代表通用 Agent 能力。
效率指标——完成相同任务的步数、token 消耗、端到端时间。两个 Agent 都能完成任务,但一个用 5 步另一个用 50 步,工程价值完全不同。
鲁棒性——相同任务多次运行的成功率方差。如果一个 Agent 成功率 80% 但方差极大(有时 1 步完成有时 100 步失败),在生产环境中的可靠性就很差。
安全性——是否会执行危险操作、是否会泄露敏感信息、是否能抵抗 prompt injection。这类评测需要专门的对抗性测试集。
评测的终极目标不是得到一个分数,而是定位瓶颈:是模型推理能力不足、是工具描述不够清晰、是上下文管理策略有问题、还是规划能力欠缺?只有定位到具体瓶颈,才能针对性优化。
九、设计原则
从简单开始,按需引入复杂性。先用单 Agent + ReAct + 少量工具验证核心假设,遇到明确瓶颈后再引入多 Agent、复杂规划、长期记忆等机制。过早引入复杂架构不会让系统更强大,只会让它更难调试——而 Agent 系统的调试难度本身就远高于传统软件。
Harness 的工程质量决定 Agent 的可靠性上限。模型能力是基础,但在相同模型上,一个精心设计的 harness(更好的工具描述、更精准的上下文管理、更完善的验证检查点、更智能的重试策略)能带来 20-50% 的绝对性能提升。这个提升完全来自工程优化,不需要等待更强的模型。
上下文是最稀缺的资源。像管理内存一样管理上下文窗口——每一个注入的 token 都必须证明自己对当前决策有正向贡献。宁可留出余量让模型"思考",也不要填满窗口让模型在信息海洋中迷失。
安全是架构约束,不是后置补丁。权限模型、沙箱隔离、行为护栏必须在架构设计阶段确定。事后补安全就像给已经建好的房子加承重墙——技术上可能做到,但成本和风险都远高于一开始就设计好。
可观测性决定可维护性。无法修复看不见的问题。Agent 的概率性行为意味着"它有时候会出错"是常态而非异常,关键是出错时能快速定位根因。每一个决策节点都应留下可审计的痕迹。
#大模型##java##python##agent开发#