Agent Harness 架构解析:从定义到解耦演进
Agent Harness 架构解析:从定义到解耦演进
还没有评论。
本文基于 Anthropic - Scaling Managed Agents 和 LangChain - The Anatomy of an Agent Harness 两篇文章,结合个人对 Claude Code 源码的理解,聊聊 Agent Harness 到底是什么、怎么设计、以及它的演进方向。
LangChain 给出的公式很简洁:
Agent = Model + Harness一句话总结:如果你不是模型,那你就是 Harness。
Harness 就是模型之外的一切——调用模型的循环、工具路由、上下文管理、执行环境、安全隔离、记忆机制……所有让一个"裸模型"变成"能干活的 Agent"的基础设施代码,统称为 Harness。
不少早期 Agent 框架(典型如早期 LangChain 的 Chain/Agent 抽象)更倾向于:把模型能力封装成 预定义的工具链,由开发者编排调用顺序。
Harness 的侧重点通常不同:它更少预设模型要做什么,而是提供 通用能力原语(文件系统、代码执行、沙箱、网络访问),让模型自主决定如何组合这些原语来解决问题。
打个比方:
Claude Code 本身就是一个典型的 Harness:
┌───────────────────────────────────────────────┐
│ Claude Code (Harness) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ System │ │ Tool │ │ Context │ │
│ │ Prompt │ │ Router │ │ Mgmt │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │CLAUDE.md │ │ Bash │ │ Memory │ │
│ │ Loader │ │ Read/Edit│ │ Files │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────┐ │
│ │ Claude Model │ │
│ │ (the Brain) │ │
│ └──────────────┘ │
└───────────────────────────────────────────────┘模型负责"想"(推理、规划、决策),Harness 负责"做"(执行命令、读写文件、管理上下文、隔离权限)。
LangChain 文章总结了 Harness 的六大核心组件,我结合 Claude Code 逐一说明。
文件系统是 Harness 最关键的基础原语,作用远超"读写文件":
| 用途 | 说明 | Claude Code 实例 |
| 持久化存储 | 跨对话运行保留信息 | CLAUDE.md |
| 上下文外扩 | 存放超出 token 限制的信息 | 工具大段输出落盘,context 中只留摘要与文件路径 |
| 多 Agent 协作面 | 共享工作区 | 团队共享的 task list 文件 |
| 版本控制 | 追踪变更、支持回退 | git worktree 隔离 |
补充说明:表格第二行里我称之为 persisted-output 的工具输出落盘策略,以及第三行里"由 TeamCreate 创建团队并绑定共享 task list 文件"的机制,均来自我对 Claude Code 源码的个人观察与推断,并非 Anthropic / LangChain 官方文档中的表述,也不是业内通用术语。
文件系统本质上是 Agent 的 外部记忆体——context window 是"工作记忆"(短期),文件系统是"长期记忆"。
与其给模型预封装 100 个工具,不如给它一个通用的代码执行能力:
预封装工具方式:
Tool: search_files(pattern="*.go", keyword="func main")
代码执行方式:
Bash: find . -name "*.go" | xargs grep "func main"后者更灵活——模型可以自己组合任意 shell 命令来解决问题,不受预定义工具集的限制。Claude Code 提供 Bash 工具正是这个设计理念:给模型"手"而非"工具箱"。
沙箱提供三层价值:
Agent 需要跨对话运行积累经验,也需要获取训练数据之外的实时信息:
run 1: 用户纠正了 coding style 偏好
↓ 写入 memory
run 2: Agent 直接按正确风格编码两条路径:
Claude Code 的记忆层次:
长对话中 context window 会"腐烂"(context rot):越来越多无关信息稀释了有效信息。Harness 需要主动管理上下文:
真实场景里,一个任务可能跨越多次对话运行、持续几小时甚至几天。Harness 需要支撑这种"长程作业":
长程执行本质上是把前面五项组件(文件系统、代码执行、沙箱、记忆搜索、上下文管理)编排起来,让 Agent 具备"跨时段推进一件事"的能力。
这部分是 Anthropic 文章的核心洞见——Harness 设计如何随模型能力进步而演进。
最初的 Managed Agents 架构把所有组件塞进一个容器:
┌──────────────────── Container ─────────────────────┐
│ │
│ Session State + Harness + Sandbox │
│ (all coupled together) │
│ │
└────────────────────────────────────────────────────┘这就像养了一只"宠物"——每个容器都是独一无二的,包含不可替代的状态。问题:
术语澄清:下文出现的 Session(首字母大写)特指 Anthropic 架构里的事件日志组件,是一个 append-only 的持久化存储,与前面章节里表示"一次对话运行"的 session 不是同一个东西。为避免混淆,本文后半部分涉及"对话运行"的地方都改用"对话运行 / 运行 / run",组件层面统一用大写 Session 或"事件日志"。
Anthropic 的解法是将单体架构拆成三个独立接口:
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Session │ │ Harness │ │ Sandbox │ │
│ │ │ │ │ │ │ │
│ │ Immutable │◄─────►│ Call LLM │──────►│ Run code │ │
│ │ event log │ │ Tool route│ │ File I/O │ │
│ │ │ │ │ │ │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ Persistent store Stateless svc Stateless sandbox │
│ (append-only) (restartable) (replaceable) │
│ │
└─────────────────────────────────────────────────────────────┘(Session = 不可变的事件日志;Harness = 调用模型 / 路由工具的无状态服务;Sandbox = 执行代码 / 文件操作的无状态容器。)
Session ↔ Harness 的交互语义:Harness 向 Session append 事件日志;重启后新实例通过 replay 已有事件流恢复状态。
这个设计借鉴了操作系统的思路——就像 OS 将硬件虚拟化为"进程"和"文件"这些比硬件寿命更长的抽象一样,Managed Agents 将 Agent 运行时虚拟化为比任何具体实现都长寿的接口。
解耦后最核心的变化是 Brain(Claude + Harness)与 Hands(Sandbox 以及各类执行工具)彻底分离:
Harness 与对话运行的状态解耦,不再和某一个具体容器绑定(harness 自身仍然可以跑在容器里,只是它变成无状态、可重启的服务)。容器这一端变成了纯粹的"手"——无状态、可替换。如果一个容器挂了,Harness 只需将其视为一次工具调用失败,重新分配一个新容器即可。
Session 独立于 Harness 存在。Harness 崩溃不会丢失任何状态,新的 Harness 实例拿到 sessionId 就能从事件日志里接着干:
// 示意伪代码(非官方 SDK 接口,仅表达恢复语义)
const session = await getSession(sessionId);
const events = await session.getEvents({ after: lastProcessedEvent });
// 从中断点继续工作解耦带来了显著的性能改善——关键指标是 TTFT(Time-to-First-Token),即用户从发送消息到看到第一个响应的时间:
| 指标 | 改善幅度 |
| p50 TTFT | ~60% 降低 |
| p95 TTFT | >90% 降低 |
核心原因(基于架构推断):解耦前,推理被绑在容器生命周期上,必须等容器就绪;解耦后,Harness 作为无状态服务可以立即开始推理,不再阻塞于执行环境就绪。
解耦还天然形成了安全隔离:
┌──────── Harness ────────┐ ┌──────── Sandbox ────────┐
│ │ │ │
│ User credentials [Y] │ │ User credentials [N] │
│ OAuth tokens [Y] │─────► │ Only op results │
│ API keys [Y] │ │ │
│ │ │ Model-generated code │
│ │ │ runs here │
└─────────────────────────┘ └─────────────────────────┘(左侧 Harness 持有用户 credentials / OAuth tokens / API keys;沙箱里只能看到操作结果,credentials 本身不进入沙箱,模型生成的代码也只在沙箱内执行。)
Anthropic 文中提到两种方向确保 credentials 不进入沙箱(以下具体落地细节是我基于架构契约做的推测,原文并未展开到这一层):
解耦后的架构给"多对多"扩展留下了空间(以下结构是基于接口契约的推演,并非 Anthropic 原文给出的明确结论):
Brain A ──┐ ┌── Hand 1 (container)
├── Session ──┐ │
Brain B ──┘ └─────┼── Hand 2 (container)
│
└── Hand 3 (MCP Server)顺着 Anthropic "稳定接口 + 可替换实现"的思路再往前推一步,会发现一个更有意思的问题:Harness 难免会编码对模型能力的假设,而这些假设会随模型进步而过时(这一层是我在原文解耦思想上的延伸,并非 Anthropic 文章直接给出的结论)。
举一个容易理解的场景(以下为说明性假想例子,用来演示"假设会过期"这一模式,并非原文出现的具体案例):假设早期模型存在"上下文焦虑"——对话变长、接近上下文上限时倾向于提前"收尾"。为此 Harness 加入上下文重置 + 结构化交接(handoff artifact)机制来规避这类行为。当换上上下文处理能力更强的新模型时,这套 workaround 可能就会从必要兜底退化成无意义的复杂度,甚至反过来拖累模型。
教训:面向模型限制写的 Harness 代码,是技术债务的温床。
Anthropic 原文给出的应对策略是 Future-Proofing Through Abstraction——构建一组比任何具体 Harness 实现都长寿的稳定接口。顺着这个思路往前推一步,可以把它看作一种"元接口层"的设计(下图中的命名是个人对这一思路的概括,并非原文术语):
稳定接口层(Session / Sandbox 契约不变) │ ├── Claude Code Harness(今天的实现) ├── Task-Specific Harness(针对特定场景优化) └── Future Harness(随模型进步而演进)
如果你在构建 Agent 应用,以下几点值得思考:
1. 不要在 Harness 里 encode 太多假设
// Bad: 假设模型不擅长长上下文
if (context.length > 4000) {
context = summarize(context); // 强制压缩
}
// Better: 让模型自己判断是否需要压缩
tools.push({
name: "compact_context",
description: "当你觉得上下文太长时调用"
});2. 从第一天就考虑对话运行的持久化
哪怕是单机应用,也应该把对话历史存到文件/数据库,而非仅保留在内存中。好处:
3. 安全边界设计前置
Credentials 管理要从架构层面解决,而非靠"小心编码"。核心原则:模型生成的代码能触及的环境里,不应该存在任何敏感信息。
4. 为模型能力增长留空间
今天你可能需要复杂的 ReAct 循环、Chain-of-Thought 强制、多步验证。但随着模型能力提升,这些可能都变成多余的。设计时问自己:如果模型突然变强 10 倍,我的 Harness 哪些部分会变成废代码?
| 维度 | LangChain 视角 | Anthropic 视角 |
| 关注点 | Harness 是什么(组件与职责) | Harness 怎么演进(解耦与架构) |
| 核心观点 | Agent = Model + Harness | 将 Brain 从 Hands 中解耦 |
| 设计思路 | 提供通用原语而非预定义工具 | 构建比实现更长寿的接口 |
| 对模型的态度 | 赋能模型自主解决问题 | 不要编码对模型的假设 |
| 关键创新 | Filesystem 作为核心原语 | Session/Harness/Sandbox 三层分离 |
两篇文章共同指向一个趋势:Harness 在变"薄"。随着模型越来越聪明,Harness 的职责从"引导模型做对事"逐渐转向"给模型提供做事的能力"。最好的 Harness,是模型感觉不到它存在的那种。