Agent Harness 架构解析:从定义到解耦演进
Agent Harness 架构解析:从定义到解耦演进
本文基于 Anthropic - Scaling Managed Agents 和 LangChain - The Anatomy of an Agent Harness 两篇文章,结合个人对 Claude Code 源码的理解,聊聊 Agent Harness 到底是什么、怎么设计、以及它的演进方向。
一、什么是 Agent Harness
1.1 一句话定义
LangChain 给出的公式很简洁:
Agent = Model + Harness一句话总结:如果你不是模型,那你就是 Harness。
Harness 就是模型之外的一切——调用模型的循环、工具路由、上下文管理、执行环境、安全隔离、记忆机制……所有让一个"裸模型"变成"能干活的 Agent"的基础设施代码,统称为 Harness。
1.2 Harness vs 传统 Agent 框架
不少早期 Agent 框架(典型如早期 LangChain 的 Chain/Agent 抽象)更倾向于:把模型能力封装成 预定义的工具链,由开发者编排调用顺序。
Harness 的侧重点通常不同:它更少预设模型要做什么,而是提供 通用能力原语(文件系统、代码执行、沙箱、网络访问),让模型自主决定如何组合这些原语来解决问题。
打个比方:
- 传统框架 ≈ 给模型一份 SOP 手册,按步骤执行
- Harness ≈ 给模型一间装备齐全的工作间,自己决定用什么工具
1.3 一个具体的例子
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 负责"做"(执行命令、读写文件、管理上下文、隔离权限)。
二、Harness 的核心组件
LangChain 文章总结了 Harness 的六大核心组件,我结合 Claude Code 逐一说明。
2.1 文件系统(Filesystem)
文件系统是 Harness 最关键的基础原语,作用远超"读写文件":
| 用途 | 说明 | Claude Code 实例 |
| 持久化存储 | 跨对话运行保留信息 | CLAUDE.md |
| 上下文外扩 | 存放超出 token 限制的信息 | 工具大段输出落盘,context 中只留摘要与文件路径 |
| 多 Agent 协作面 | 共享工作区 | 团队共享的 task list 文件 |
| 版本控制 | 追踪变更、支持回退 | git worktree 隔离 |
补充说明:表格第二行里我称之为 persisted-output 的工具输出落盘策略,以及第三行里"由 TeamCreate 创建团队并绑定共享 task list 文件"的机制,均来自我对 Claude Code 源码的个人观察与推断,并非 Anthropic / LangChain 官方文档中的表述,也不是业内通用术语。
文件系统本质上是 Agent 的 外部记忆体——context window 是"工作记忆"(短期),文件系统是"长期记忆"。
2.2 代码执行(Code Execution)
与其给模型预封装 100 个工具,不如给它一个通用的代码执行能力:
预封装工具方式:
Tool: search_files(pattern="*.go", keyword="func main")
代码执行方式:
Bash: find . -name "*.go" | xargs grep "func main"后者更灵活——模型可以自己组合任意 shell 命令来解决问题,不受预定义工具集的限制。Claude Code 提供 Bash 工具正是这个设计理念:给模型"手"而非"工具箱"。
2.3 沙箱(Sandbox)
沙箱提供三层价值:
- 安全性:模型生成的代码在隔离环境中执行,不会影响宿主系统
- 可扩展性:沙箱可以独立伸缩,不受 Harness 进程约束
- 预装环境:语言运行时、CLI 工具、浏览器等,让 Agent 能自主验证工作结果
2.4 记忆与搜索(Memory & Search)
Agent 需要跨对话运行积累经验,也需要获取训练数据之外的实时信息:
run 1: 用户纠正了 coding style 偏好
↓ 写入 memory
run 2: Agent 直接按正确风格编码两条路径:
- 持久化记忆文件:把偏好、规范、经验写入文件,跨运行复用
- 外部搜索能力:通过 web search、MCP 工具等访问训练截止之后的信息
Claude Code 的记忆层次:
- ~/.claude/CLAUDE.md — 用户全局偏好
- 项目级 CLAUDE.md — 项目规范
- 单次运行的工作上下文 — 本轮对话与工具结果,默认不进入长期 memory 文件,下次启动也不会自动读到(Claude Code 会把 transcript 落盘到 ~/.claude/projects/... 供恢复与排查,但不是模型下轮默认能看到的"记忆")
2.5 上下文管理(Context Management)
长对话中 context window 会"腐烂"(context rot):越来越多无关信息稀释了有效信息。Harness 需要主动管理上下文:
- 压缩(Compaction):压缩历史对话,保留关键信息
- 工具输出卸载(Offloading):大段输出存到文件而非留在 context 中
- 渐进式技能披露(Progressive Skill Disclosure):按需注入指令,而非一次性全部加载
2.6 长程执行(Long-Horizon Execution)
真实场景里,一个任务可能跨越多次对话运行、持续几小时甚至几天。Harness 需要支撑这种"长程作业":
- 文件系统作为进度载体:把中间状态落盘,运行中断也不会从头再来
- 规划与自我验证:让 Agent 拆解子任务、检查阶段性产出
- 可恢复的执行循环:例如 Ralph Loop 这类模式——中断后可以基于任务清单与已产出物继续推进,而不是重跑
长程执行本质上是把前面五项组件(文件系统、代码执行、沙箱、记忆搜索、上下文管理)编排起来,让 Agent 具备"跨时段推进一件事"的能力。
三、Harness 的架构演进
这部分是 Anthropic 文章的核心洞见——Harness 设计如何随模型能力进步而演进。
3.1 早期:单容器 “Pet” 模式
最初的 Managed Agents 架构把所有组件塞进一个容器:
┌──────────────────── Container ─────────────────────┐
│ │
│ Session State + Harness + Sandbox │
│ (all coupled together) │
│ │
└────────────────────────────────────────────────────┘这就像养了一只"宠物"——每个容器都是独一无二的,包含不可替代的状态。问题:
- 容器挂了 = 会话丢失:所有状态随容器一起消失
- 调试困难:进入容器调试可能暴露用户数据
- 启动慢:用户发第一条消息要等容器就绪,p95 延迟极高
- 弹性差:无法独立扩展计算和存储
3.2 三层解耦:Session / Harness / Sandbox
术语澄清:下文出现的 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 运行时虚拟化为比任何具体实现都长寿的接口。
3.3 Brain vs Hands 分离
解耦后最核心的变化是 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 });
// 从中断点继续工作3.4 性能收益
解耦带来了显著的性能改善——关键指标是 TTFT(Time-to-First-Token),即用户从发送消息到看到第一个响应的时间:
| 指标 | 改善幅度 |
| p50 TTFT | ~60% 降低 |
| p95 TTFT | >90% 降低 |
核心原因(基于架构推断):解耦前,推理被绑在容器生命周期上,必须等容器就绪;解耦后,Harness 作为无状态服务可以立即开始推理,不再阻塞于执行环境就绪。
3.5 安全边界
解耦还天然形成了安全隔离:
┌──────── 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 不进入沙箱(以下具体落地细节是我基于架构契约做的推测,原文并未展开到这一层):
- 初始化时绑定(bundling):credentials 在容器初始化时绑定为资源(例如 Git repo token 绑为 repo 资源的一部分),沙箱只能通过上层抽象(如 git 命令)隐式使用
- 代理访问(external vault):工具调用通过专用代理获取 credentials,代理从外部 vault 读取后只把操作结果回传,credentials 本体不进入 sandbox、也不会暴露给模型生成的代码
3.6 多 Brain 多 Hands
解耦后的架构给"多对多"扩展留下了空间(以下结构是基于接口契约的推演,并非 Anthropic 原文给出的明确结论):
Brain A ──┐ ┌── Hand 1 (container)
├── Session ──┐ │
Brain B ──┘ └─────┼── Hand 2 (container)
│
└── Hand 3 (MCP Server)- 多 Harness 可以作为无状态服务连接到同一个 Session
- 多 Sandbox 被视为可互换的工具
- 理论上 Brain 之间还可以相互"传递" Hands,从而支持更复杂的多 Agent 协作
四、设计哲学与启示
4.1 Harness 的"过期"问题
顺着 Anthropic "稳定接口 + 可替换实现"的思路再往前推一步,会发现一个更有意思的问题:Harness 难免会编码对模型能力的假设,而这些假设会随模型进步而过时(这一层是我在原文解耦思想上的延伸,并非 Anthropic 文章直接给出的结论)。
举一个容易理解的场景(以下为说明性假想例子,用来演示"假设会过期"这一模式,并非原文出现的具体案例):假设早期模型存在"上下文焦虑"——对话变长、接近上下文上限时倾向于提前"收尾"。为此 Harness 加入上下文重置 + 结构化交接(handoff artifact)机制来规避这类行为。当换上上下文处理能力更强的新模型时,这套 workaround 可能就会从必要兜底退化成无意义的复杂度,甚至反过来拖累模型。
教训:面向模型限制写的 Harness 代码,是技术债务的温床。
4.2 面向稳定接口而非面向实现
Anthropic 原文给出的应对策略是 Future-Proofing Through Abstraction——构建一组比任何具体 Harness 实现都长寿的稳定接口。顺着这个思路往前推一步,可以把它看作一种"元接口层"的设计(下图中的命名是个人对这一思路的概括,并非原文术语):
- 对接口有强主张:Session 必须是 append-only 的事件流、Sandbox 必须是无状态可替换的
- 对实现保持中立:具体的 Harness 逻辑(Claude Code、task-specific agent、未来新形态)可以随意替换
稳定接口层(Session / Sandbox 契约不变) │ ├── Claude Code Harness(今天的实现) ├── Task-Specific Harness(针对特定场景优化) └── Future Harness(随模型进步而演进)
4.3 对我们的启发
如果你在构建 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,是模型感觉不到它存在的那种。