从 OpenClaw 看 Agent 架构设计 一、引言构建一个 Agent 需要做一系列架构决策上下文怎么管理工具怎么加载工具怎么查找Agent 的主循环围绕什么来设计这些决策没有标准答案但每个选择都有明确的代价。本文从 OpenClaw、Claude Code 等主流 Agent 产品的实现出发拆解四个关键的设计决策分析各种方案的利弊。二、上下文管理2.1 追加式上下文OpenClaw 和 Claude Code 采用了同一种模式追加式上下文Append-only Context。Agent 维护一个持续增长的对话历史每次调用 LLM 时将完整历史作为 prompt 发送每轮交互的结果追加到同一个数组中。第1轮 LLM 调用 ┌──────────────────────────────────────────────┐ │ System Prompt │ User: 帮我重构登录模块 │ → LLM → 回复 工具调用 └──────────────────────────────────────────────┘ ~2K tokens 第2轮 LLM 调用工具结果返回后 ┌──────────────────────────────────────────────────────────────────┐ │ System Prompt │ User │ Assistant │ Tool Call │ Tool Result │ ... │ → LLM └──────────────────────────────────────────────────────────────────┘ ~8K tokens ... 经过 20 轮工具调用 ... 第N轮 LLM 调用 ┌─────────────────────────────────────────────────────────────────────────────┐ │ System │ U │ A │ T │ R │ A │ T │ R │ A │ T │ R │ ... │ User: 你好 │ └─────────────────────────────────────────────────────────────────────────────┘ │◄──────────────── 80K tokens 历史 ──────────────────────►│◄─ 2 tokens ──►│ 全部重新发送 实际新内容这种模式运行在一个典型的 Agent Loop 中用户输入 │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Agent Loop │ │ │ │ ┌───────────────────────────────────────────────┐ │ │ │ messages [system, ...history, user_input] │ │ │ └───────────────────┬───────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ 调用 LLM API │◄────────────┐ │ │ │ (全量上下文) │ │ │ │ └────────┬────────┘ │ │ │ │ │ │ │ ▼ │ │ │ 有工具调用 │ │ │ ╱ ╲ │ │ │ 是 否 │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ ┌────────────┐ ┌──────────┐ │ │ │ │ 执行工具 │ │ 输出回复 │ │ │ │ │ 追加结果 │ │ (结束) │ │ │ │ │ 到 messages │ └──────────┘ │ │ │ └─────┬──────┘ │ │ │ │ 追加到 messages │ │ │ └──────────────────────────────────┘ │ │ │ │ messages 只增不减直到会话结束或达到上下文窗口上限 │ └─────────────────────────────────────────────────────────┘优势缓存利用率高——每次请求的前缀高度重叠prompt cache 命中率极高实现简单——一个数组只追加不修改不需要复杂的上下文选择或裁剪逻辑上下文连贯——模型始终能看到完整的交互历史对单一任务的长期推理非常理想问题当用户在同一个会话中切换话题时所有历史被无差别地塞入同一个上下文窗口。用户上午讨论了一个复杂的代码重构消耗 80K token下午回来说了句你好——这句你好需要携带 80K token 的完整历史一起发送给模型。即便命中缓存以 Opus 定价计算也要 $0.30。上下文只增不减无关信息干扰模型推理成本与当前请求的复杂度完全脱钩。2.2 压缩策略追加式模式终究会撞上上下文窗口的天花板。Claude Code 和 OpenClaw 各自设计了压缩机制。Claude Code在上下文使用率达到约 83.5% 时自动触发压缩——优先裁剪工具输出LLM 生成结构化摘要前置到新会话开头最近几轮对话原样保留。压缩前 ┌─────────────────────────────────────────────────────────────────┐ │ System │ U │ A │ T │ R │ ... │ U │ A │ T │ R │ U │ A │ U │ A │ └─────────────────────────────────────────────────────────────────┘ │◄──────────────────── ~167K tokens ─────────────────────────────►│ 压缩后 ┌─────────────────────────────────────────────────────────────────┐ │ System │ [压缩摘要: 已完成X, 正在做Y, │ 最近几轮 │ 新输入 │ │ │ 修改了哪些文件, 关键决策...] │ 原样保留 │ │ └─────────────────────────────────────────────────────────────────┘ │◄─ 固定 ─►│◄──── ~10-20K ──────────────►│◄─ 近期 ─►│OpenClaw采用更复杂的多层策略在压缩之前先进行记忆落盘让模型把重要信息写入磁盘上的 .md 文件然后分阶段渐进压缩。落盘的记忆文件跨会话存活下次可加载。压缩机制是必要的安全网但它是对上下文膨胀的缓解而非解决压缩本身需要额外的 LLM 调用摘要是有损的而且压缩后的摘要仍然混合了所有话题。这引出了另一个思路与其在上下文膨胀之后想办法缩小能否从源头避免不相关的内容进入同一个上下文2.3 任务隔离另一种思路除了压缩还有一种更直接的应对方式以任务为单位隔离上下文。这并不是什么新概念——传统的任务队列、工单系统早就是这么做的只是在 Agent 领域对话驱动模式的流行让这种思路被忽略了。对话驱动当前主流模式 ┌──────────────────────────────────────────────────────────────┐ │ 重构登录模块(80K) │ 你好(2) │ 查询用户数据(30K) │ ... │ │ 所有内容挤在同一个上下文中 │ └──────────────────────────────────────────────────────────────┘ 每次调用都携带全部历史无论是否相关 任务隔离 ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ 任务1: 重构登录 │ │ 任务2: 查询数据 │ │ 任务3: 部署服务 │ │ 上下文: 80K │ │ 上下文: 30K │ │ 上下文: 15K │ │ (只含相关内容) │ │ (只含相关内容) │ │ (只含相关内容) │ └──────────────────┘ └──────────────────┘ └──────────────────┘ 每个任务只为自己的上下文付费以任务为单位隔离上下文每个任务拥有独立的上下文窗口。任务内部仍然使用追加式上下文——这完全合理一个任务内的所有交互记录天然相关。但任务之间的上下文是隔离的互不干扰。这种方式要求用户建立一个简单的意识不同的问题在不同的任务中解决。就像程序员知道不同的功能应该在不同的分支上开发一样。好的设计应该让这种隔离自然发生——新问题自动创建新任务上下文跟着任务走用户不需要手动管理 prompt。任务隔离的优势是彻底消除了话题混杂问题每个任务的上下文成本与其自身的复杂度成正比。代价是放弃了跨任务的上下文连贯性——如果两个任务之间有关联需要额外的机制如任务间共享摘要、会话关联来传递信息。对话驱动模式屏蔽了任务概念换来的是入门门槛低任务隔离模式引入了任务边界换来的是更高的效率和更低的成本。两者的取舍取决于目标用户和使用场景。三、工具加载3.1 tools 字段与缓存机制的冲突LLM API 的请求中tools 是独立于 messages 的顶层字段。模型针对这种结构化格式专门微调过保证了工具调用的高正确率和解析确定性。但 Anthropic 的 Prompt Cache 按严格顺序构建缓存前缀 system → tools → messages。 tools位于前端缓存从第一个 token 开始逐一比对任何位置出现差异该位置及之后的所有内容全部失效。请求 A5 个工具 ┌──────────────────────────────────────────────────────────┐ │ system prompt │ tools(5个) │ msg1 │ msg2 │ ... │ msg100 │ └──────────────────────────────────────────────────────────┘ ✅ 全部命中 请求 B加了1个新工具变成6个 ┌───────────────────────────────────────────────────────────┐ │ system prompt │ tools(6个) │ msg1 │ msg2 │ ... │ msg100 │ └───────────────────────────────────────────────────────────┘ ↑ tools 变了整条链全部失效 ❌ 100K token 的 messages 缓存失效即使内容完全没变MCP 的核心价值在于动态工具发现和加载但每切换一次工具集就要为整个对话历史全额付费。Claude Code 报告的 92% 缓存命中率很大程度上靠的就是永远不改变 tools 列表。当前架构的矛盾 动态工具MCP 的核心价值 ↕ 不可调和 缓存稳定性成本控制的关键3.2 工具描述从 tools 字段移入 prompt一种替代方案是将工具描述嵌入 messages 而非 tools 字段当前方式tools 字段注入 ┌──────────────────────────────────────────────────┐ │ system │ tools [A,B,C] │ messages... │ └──────────────────────────────────────────────────┘ ↑ 改这里 → 后面全部失效 替代方式工具描述嵌入 prompt ┌──────────────────────────────────────────────────┐ │ system │ messages... │ [工具 A 描述] [工具 B 描述] │ └──────────────────────────────────────────────────┘ ↑ 追加新工具 → 前面的缓存全部保留 ✅优势是增加工具不破坏缓存按需加载与追加式上下文兼容。代价是离开了 tools 字段的结构化保证需要自行实现工具结构的解析和调用逻辑模型从纯文本描述中理解自定义 API schema 的可靠性可能下降。3.3 本地用控制台远程用 MCP工具描述移入 prompt 后格式可靠性取决于工具类型。如果工具是终端—— execute: 执行 shell 命令返回 stdout/stderr ——几乎不需要教模型任何东西。curl、grep、psql、docker 这些是模型在预训练中已经掌握的世界知识不是需要通过 JSON Schema 定义的陌生 API。MCP 方式需要 N 个工具定义 ┌─────────────────────────────────────────────────────────┐ │ tools: [get_weather, query_db, send_email, read_file, │ │ write_file, search_web, translate, ...] │ └─────────────────────────────────────────────────────────┘ 每增加一个 → 缓存全部失效 控制台方式始终只需要 1 个工具定义 ┌───────────────────────────┐ │ tools: [execute] │ ← 永远不变永远命中缓存 └───────────────────────────┘ 需要查天气 → execute: curl wttr.in/Beijing 需要查数据库→ execute: psql -c SELECT ... 需要发邮件 → execute: sendmail ...控制台的优势零学习成本、格式解析问题消失、工具数量无限、与缓存完全兼容。但控制台不能覆盖所有场景——需要 OAuth 认证的第三方 API、需要维持长连接的服务等终端命令并非最佳选择这类远程服务仍然需要 MCP 提供标准化的接口结构和鉴权机制。实际的工具策略是本地能力用控制台远程能力用 MCP二者组合覆盖绝大部分场景。少数需要持久连接或复杂状态管理的场景如 WebSocket 订阅、连接池维护仍然是开放问题。3.4 渐进式工具加载结合控制台和 MCP理想的工具加载方式是渐进式的任务开始轻量上下文 ┌────────────────────────────────────────────┐ │ system │ 任务目标 │ 基础工具(execute) │ └────────────────────────────────────────────┘ ~3K tokens Agent 判断需要远程搜索工具 ┌────────────────────────────────────────────────────────────┐ │ system │ 任务目标 │ 基础工具 │ 对话... │ mcp_search 描述 │ └────────────────────────────────────────────────────────────┘ │◄────────── 缓存命中 ──────────────►│◄─ 新增 ─►│ Agent 发现并加载数据库工具 ┌──────────────────────────────────────────────────────────────────────┐ │ system │ 任务目标 │ 基础工具 │ 对话... │ mcp_search │ query_db 描述 │ └──────────────────────────────────────────────────────────────────────┘ │◄─────────────── 缓存命中 ──────────────────────►│◄── 新增 ──►│每次只追加新工具的描述已有的上下文缓存完全保留——工具跟着对话流走而不是钉死在缓存前缀的头部。3.5 两种加载方式的对比两种方式可以混合使用高频、核心的工具如 execute放在 tools 字段保证可靠性和缓存稳定低频、动态发现的远程工具通过 prompt 渐进式加载避免破坏缓存前缀。四、工具查找当可用工具达到数百甚至上千个时Agent 如何高效地找到解决当前问题所需的工具4.1 四种方式及其取舍全量注入 tools 字段。最简单模型路由准确率高。但工具列表变动导致缓存失效工具数量多时固定 token 开销巨大。追加上下文加载。不破坏缓存但工具描述一旦追加就永远留在上下文中几轮搜索下来上下文堆满已用完的工具描述。子 Agent 查找。不污染主上下文但子 Agent 缺乏主对话的上下文理解可能选择不够精准的工具。向量搜索 / 关键词检索。零 LLM token 消耗但语义模糊时不够准确工具库大时噪声严重。这四种方式面临一个共同的矛盾我们在接口维度上查找工具一个 API 一个工具但实际需求是在功能维度上查找能力。4.2 Skill按功能维度组织工具Skill 的本质是将多个工具的调用方法内聚在一起按功能维度组织而非按接口维度罗列。接口维度传统工具列表 ┌────────────────────────────────────────────────────────────┐ │ pg_connect │ pg_query │ pg_insert │ pg_update │ │ pg_delete │ pg_schema │ pg_export │ pg_import │ │ redis_get │ redis_set │ redis_del │ redis_scan │ │ s3_upload │ s3_download│ s3_list │ s3_delete │ └────────────────────────────────────────────────────────────┘ 20 个工具定义搜索空间20 个选项 功能维度Skill ┌──────────────────────────────────────────────┐ │ 数据库管理 │ 缓存操作 │ 文件存储 │ │ (内含 8 个 │ (内含 4 个 │ (内含 4 个 │ │ pg_* 工具的 │ redis_* │ s3_* 工具的 │ │ 组合用法) │ 工具用法) │ 组合用法) │ └──────────────────────────────────────────────┘ 3 个技能搜索空间3 个选项一个 Skill 不是一个工具而是一份如何组合使用多个工具完成某类任务的说明书# 数据库管理技能 ## 能力描述 管理 PostgreSQL 数据库查询、写入、schema 管理、数据导入导出。 ## 可用工具 - psql命令行客户端支持 SQL 查询和管理 - pg_dump / pg_restore备份与恢复 ## 常见用法 ### 查询数据 psql -h $HOST -d $DB -c SELECT * FROM users WHERE active true ### 导出为 CSV psql -h $HOST -d $DB -c \COPY (SELECT ...) TO STDOUT CSV HEADER output.csv ### 查看表结构 psql -h $HOST -d $DB -c \d tablename ## 注意事项 - 写操作前先确认避免误修改 - 大查询加 LIMIT4.3 Skill 是工具调用知识的 Cache就像 CPU 缓存把频繁访问的内存数据放在更快的存储中一样Skill 把频繁使用的工具调用知识预先组织好避免每次都从头搜索和学习。没有 Skill每次从头探索 任务: 导出用户数据为 CSV Agent 第1次做搜索工具 → 阅读文档 → 尝试执行 → 调试 → 成功 Agent 第2次做同样的探索过程再来一遍 每次都消耗大量 token 在重新发现已知的方法 有 Skill知识已缓存 任务: 导出用户数据为 CSV Agent 任意次做匹配技能 → 加载 → 直接执行 1 次搜索 加载技能文本 ≈ 几百 token技能不一定需要人工编写。Agent 可以在首次完成新类型任务后将探索过程中学到的工具组合自动整理成 Skill下次遇到同类任务直接加载。这形成了一个自我优化的循环Agent 使用工具越多积累的 Skill 越丰富未来的效率越高。4.4 综合对比Skill 不是替代其他方式而是在它们之上提供了一个缓存层。底层仍然可以用搜索检索来匹配 Skill用追加上下文来加载 Skill 内容但搜索空间和加载量都被功能聚合大幅压缩了。五、主循环设计前三章讨论了 Agent 的上下文、工具加载和工具查找机制其中第一章已经提到了任务隔离作为上下文管理的一种思路。本章从更上层的视角展开这个话题Agent 的主循环围绕什么来设计5.1 对话驱动当前几乎所有 Agent 产品——OpenClaw、ChatGPT、Claude——都围绕对话框设计用户在对话框里输入Agent 在对话框里回复对话框既是输入源、输出目标也是维系上下文的容器。这种模式的优势是交互门槛极低——和聊天没有区别不需要理解任何抽象概念。但它也意味着 Agent 的一切行为都被包裹在对话交互中上下文管理、工具调用、结果输出全部绑定在对话框这个载体上。5.2 任务驱动另一种思路是围绕任务来设计主循环对话驱动 ┌─────────────────────────────────────────────┐ │ 对话框 Agent 的世界 │ │ │ │ 用户输入 → Agent 思考 → Agent 回复 │ │ 用户输入 → Agent 思考 → 调用工具 → 回复 │ │ │ │ 对话框既是输入源也是输出目标 │ │ 更是维系上下文的容器 │ └─────────────────────────────────────────────┘ 任务驱动 ┌─────────────────────────────────────────────┐ │ Agent 主循环感知→思考→行动 │ │ │ │ 感知 │ │ ├─ 聊天框工具→ 读取用户消息 │ │ ├─ Cron 定时器 → 触发定期检查 │ │ ├─ Webhook → 接收外部事件 │ │ └─ 自身推理 → fork 新子任务 │ │ │ │ 思考Agent 的核心输出 │ │ 分析任务 → 制定方案 → 选择工具 │ │ │ │ 行动 │ │ ├─ 控制台工具→ 执行本地操作 │ │ ├─ MCP工具→ 调用远程服务 │ │ ├─ 聊天框工具→ 回复用户 │ │ └─ 任务队列 → 创建后续任务 │ └─────────────────────────────────────────────┘在任务驱动设计中聊天框被降格为一个工具——Agent 通过它读取用户消息也通过它回复用户和 execute控制台、MCP 远程调用没有本质区别。聊天框的特殊之处在于它同时充当输入源和输出工具但它是 Agent 众多可调用能力中的一个不是 Agent 运行的容器。5.3 思考过程作为核心输出如果聊天框只是工具之一Agent 的核心输出就不是对话回复而是思考过程对话驱动Agent 的输出 对话回复 ┌────────────────────────────────────────────┐ │ User: 帮我查一下线上报错 │ │ │ │ Assistant: 我来查一下。 │ ← 主要输出对话文本 │ [调用工具: grep error.log] │ ← 附带动作 │ Assistant: 找到了是数据库连接超时... │ ← 主要输出对话文本 └────────────────────────────────────────────┘ 任务驱动Agent 的输出 思考链 ┌────────────────────────────────────────────┐ │ 任务: 排查线上报错 │ │ │ │ 思考: 需要先查看错误日志 │ │ 行动: execute(grep -n ERROR app.log) │ │ 思考: 发现数据库连接超时需要检查连接池配置 │ │ 行动: execute(cat config/db.yaml) │ │ 思考: 连接池 max_idle 设置过低 │ │ 需要告知用户结果 │ │ 行动: chat.send(问题定位到了...) │ ← 回复用户只是其中一个动作 │ 任务完成 │ └────────────────────────────────────────────┘当思考过程成为核心输出时Agent 的行为变得可观测——用户可以看到 Agent 在想什么、为什么选择某个工具、为什么放弃某个方案。当 Agent 犯错时可以追溯到具体哪一步思考出了问题这种可追溯性是 Agent 持续改进的基础。Claude Code 在这个方向上做了有意义的尝试——它展示了思考过程、工具调用和执行结果。虽然目前的形式更像是 Agent 在自问自答但核心理念值得借鉴让思考过程成为一等公民。5.4 现实约束模型的对话训练倾向任务驱动设计面临一个现实障碍当前的大语言模型是以对话模式训练的。RLHF 优化的是回复满意度——直接给出答案几乎总是比我先调用工具获得更高的评分模型的本能是直接回答工具调用是后天习得的——function calling 能力通过后期微调加入没有明确触发信号时模型会 fallback 到对话模式回复和工具调用是竞争关系——训练数据中说话的样本远多于调用工具的样本要让 Agent 真正以思考→工具调用为主循环不仅需要 system prompt 层面的引导更需要模型厂商在训练阶段针对 agent 模式专门优化。从 GPT-4 到 Claude 到 Gemini每一代模型的工具调用能力都在提升但距离模型天然就是任务执行器的状态还有距离。5.5 两种模式的取舍两种模式并非非此即彼。对话驱动可以作为任务驱动的前端——用户通过对话提交需求系统自动将其转化为任务在后台以任务驱动模式执行。关键是在架构层面保留任务隔离的能力而不是将所有东西绑定在一个对话框上。六、总结构建 Agent 的四个关键设计决策①上下文管理。