最近系统性地研究了一下 Claude Code 的实现,作为 Java 程序员写了一个开源的 Loop-based Agent Harness 框架。从工程的角度,以 Java 语言构建一个 Harness 的学习框架,理解 Harness 实现的技术细节。
从"写提示词"到"设计循环"再到"驾驭循环"
在使用 AI 的初期,你是否遇到过这样一个场景。写一个提示词,AI 回复一次,然后人工评估 AI 的结果,修改提示词,重新再向 AI 提问。这个场景中人是 Loop 中的一个环节,人的效率决定了 Loop 的效率。
Loop Engineering 被提出。Loop 的核心是人设定目标,系统自动完成"执行-观察-评估-修正"的闭环,直到任务完成。人从"指挥者"变成了"系统设计者"。但是 Loop 带来了新的问题,Loop Engineering 解决了 "AI 能不能自己跑",但没解决 "AI 能不能跑得久、跑得稳、跑得安全、跑得起"。
为了解决以上的问题,Harness Engineering 被提出。在 Loop Engineering 基础上增加了上下文工程、工具设计、权限管控、记忆管理、上下文压缩等能力。

Harness 的核心逻辑实现
首先需要一个 Loop
Loop 的核心是:用户输入 → LLM 基于已有的 Tool 工具,规划任务,调用 Tools → 获取 Tools 的执行结果 → 判断 Loop 的退出条件。
几个核心点:
-
Tools 的管理:
-
Tools 的名称、功能描述、参数 Schema(JSON Schema)、具体的执行代码。参数 Schema 是 LLM 生成正确调用参数的依据,缺一不可。
-
需要将支持的 Tools 告诉 LLM。
- 需要一个 tools 的管理模块。
-
LLM 会识别用户的意图,规划调用的 Tool。
-
-
MessageList 的管理
- MessageList 中通常包含三类消息:user message(用户输入)、assistant message(LLM 输出,可能包含 tool_use)、tool_result message(工具执行结果)。
- 每次执行完 tools 后,需要将 tool_result message 加入 MessageList,作为下一轮 LLM 推理的上下文。
-
Loop 的退出
- 正常退出:LLM 返回的响应中不再包含 tool_use 请求(stop_reason = "end_turn"),只要 LLM 还想调用工具,循环就继续。
- 兜底机制:max_iterations 上限(防死循环)、token budget 上限(防成本失控)、用户中断信号(Ctrl+C 或前端取消)。
Tools 管理模块- Agent 与现实的连接
Tools 管理模块的主要功能:
- 工具的定义
- 处理工具的注册和分发
工具的 Schema 校验、入参校验、PreToolUse hooks、权限校验,之后逐渐实现。后续还可以关注支持并发执行、结果持久化。
权限管控-把 Tools 管起来
LLM 是不可控的,如果模型收到调用 rm -rf / 的命令,无脑执行那是非常危险的。需要在工具执行之前进行一次检查。
可以简单将权限进行归类:
- 可以放过的 :
wc -l、grep等只读操作 - 需要询问的 :
git commit、git push等 - 绝对禁止的 :
rm -rf /这种删除磁盘的 - 暂时不表态的:交给下游判断
Hooks - 增强 Tools
如果你需要在每次调用 tools 时都做一些固定的增强,比如记录工具的调用,比如上文的每次工具调用都需要进行权限检查,作为 Java 程序员很自然地想到 Spring 中的 AOP(切面的实现)。在 Harness 中,就以 Hook 的形式实现 ------ 对 Agent 的增强,且不需要对 Loop 进行改动。
TodoWrite - 为 Agent 增加计划能力
循环可以完整地执行了,但是对一个复杂任务直接动手效果并不好,可以让模型先列计划,再分步执行。
上下文越长,模型的注意力会被稀释。一个 10 步的工作,模型做了 1-3 步之后,就开始即兴发挥了。
Agent 接收到任务,先调用 todo_write 生成多个子任务并标记为 pending。执行任务时标记为 in_progress,任务执行完标记为 completed,查看下一个 pending 任务。
TodoWrite 提升了 Agent 的规划能力。
SubAgent - 需要一个干净的的上下文
Agent 在处理一个相对复杂的任务时,经过多轮对话,大部分都是中间过程,和最终的目标无关。这些中间过程占用上下文,容易使 Agent 注意力不集中,逐渐忘记最终目标。
所以换个角度思考,完成一个相对独立的工作时候,可以开一个独立的 Agent 实例(隔离的 MessageList / context),让它专心地做一件事情。
TodoWrite 用于规划;当某个子任务足够独立、上下文可以隔离时,主 Agent 可以选择 spawn 一个 SubAgent 去执行,并只回传最终结果。
核心洞察:LLM 的上下文是宝贵的。过长的上下文会导致 LLM 注意力涣散,所以之后的很多工作,都在想办法解决上下文过长的问题。
Skill 管理 - 为 LLM 增加 Tools 使用说明书
在 Tool 之外,Harness 还需要另一层能力抽象------Skill。
Skill vs Tool 的核心区别:
| 维度 | Tool | Skill |
|---|---|---|
| 本质 | 一个可执行函数 | 一份方法论 / 操作手册 |
| 调用方式 | LLM 直接调用(tool_use) | LLM 先读 markdown → 再按指令行动 |
| 粒度 | 原子操作 | 组合流程 |
| 上下文占用 | 常驻 system prompt | 按需加载(lazy) |
| 组成 | name + description + schema + code | markdown 指令(内部可组合调用多个 tool) |
| 谁定义 | 引擎 / 框架开发者 | 用户 / 领域专家(纯 markdown 即可扩展) |
用 Java 视角类比:Tool 是 @Service 里的一个 method(如 bash()、read()),Skill 是一个 Service 类或一段 SOP 文档,内部会编排多个 method;Skill 的加载对应 Spring 的 @Lazy bean,首次被用到时才加载。
为什么需要 Skill------回到"上下文是宝贵的"这个核心洞察:
如果所有能力都作为 Tool 常驻 system prompt,100 个 tool 的定义会消耗几万 token,且大部分 tool 在当前任务里根本用不到。Context 被"能力目录"塞满,真正做事的空间就变少了。
Skill loading 的机制:
- 启动时只加载 skill 的 name + 一句话描述(占用极小)
- 用户发起请求,LLM 判断需要哪个 skill
- Harness 动态读取对应 skill 的 markdown 全文,注入 context
- LLM 按 skill 里的步骤,调用底层 tool 完成任务
Skill 与 Tool 的层次关系:
bash
Skill ← 高层:方法论 / SOP(按需加载的 markdown)
│ 组合调用
▼
Tool ← 底层:原子执行单元(常驻注册的 function)
│ 落到
▼
真实操作(fs / shell / net)
一个不太准确但直观的比喻:Tool 是厨房里的刀具、锅、火 ;Skill 是一份菜谱 (教你何时用哪把刀、什么火候);LLM 是厨师 ;Harness 是整个厨房(把菜谱柜、刀架、灶台组织起来的空间)。菜谱本身不切菜,但没有菜谱厨师就会乱来。
Skill 的引入,本质上是 Harness Engineering 中的上下文分层加载机制------把"能力目录"和"能力详情"分开,按需展开,从而在有限的 context 里承载更多的能力。
上下文压缩 - 解决 LLM 注意力问题
上文多次提到,LLM 的上下文是宝贵的。随着 Loop 的轮次增加,MessageList 里面的信息越来越多,需要有一套机制处理上下文过长的问题,"上下文压缩"就被提了出来。
上下文压缩手段:
- L1:单条信息太长的,落盘,留下索引,大模型需要的时候再查询。防止过长的结果撑爆上下文。
- L2:tools result 的历史结果使用占位符代替(历史的结果模型可能不关心)。过久的结果影响模型注意力,需要时再重新调用。
- L3:多轮对话太多的,去除中间。留下开始的对话(可能包含用户的指令)和最后的对话(模型需要知道最近在干啥)。
- L4:使用 LLM 总结,提炼上下文内容。
核心思路:
- 先基于规则,使用便宜的方式
- 压缩可回溯,需要时再查询
- 最后才用 LLM 总结
Memory - 让 Agent 拥有记忆
在 Loop 中随着信息不断添加与压缩,我们还需要设计一个机制,保存需要记住的东西。
Memory = 把对话里值得永久记住的事实(fact)落到磁盘文件,跨 Compact 不丢、跨 session 仍可用。
Memory 的核心模块:
erlang
Storage ── 纯 IO,提供增删改查 API(不智能)
Selection ── LLM side-query 挑相关 memory + 关键词回退
Extraction ── LLM 从对话提取 fact 写入 Storage(无回退,失败就跳过)
Consolidation ── LLM 去重合并,解决 Memory 过大问题
Storage 是基石
Storage 是 IO 存储的抽象,提供 memory 的 CRUD 方法。目前使用文件的形式进行存储,后期可以使用数据库、RAG 等组件。
记忆的生成
每轮对话完成,通过 Extraction 组件调用 LLM 从 MessageList 里面抽取 fact,调用 Storage 的写入方法。Extraction 属于"锦上添花"型任务,无回退机制,失败即跳过,不阻塞主 Loop。
记忆的整理
Consolidation 会调用 LLM 对记忆进行整理和去重。由 memory 数量阈值触发(而非每轮同步执行),避免阻塞主 Loop。
记忆的使用
memory 的 catalog(索引 / 摘要)常驻 system prompt,全文按需加载。LLM 通过 side-query 从 catalog 中挑选相关记忆,再查询记忆全文,注入 Loop。当 side-query 未命中时,用关键词匹配作为回退,保证召回率。
与 Skill 的呼应:Memory 的加载机制与 Skill 同构 ------ catalog 常驻 system,全文按需注入。都是 Harness Engineering 中"分层加载"策略在不同数据形态上的应用:Skill 分层加载的是"能力",Memory 分层加载的是"事实"。
System prompt - 运行时组装, 不硬编码
前面几章讲了 Loop、Tool、Skill、Memory、上下文压缩,但都绕开了一个问题:这些能力是怎么"告诉" LLM 的? 答案就是 System prompt。
从"写死的模板"到"运行时配置"
初学者常把 System prompt 当成一个写死的字符串,随代码一起提交。但当 Harness 长大后,会遇到三个问题:
- 换项目要重写整个 prompt,不知道哪些该改、哪些该留(身份、工具、规范混在一起,牵一发动全身)
- 修改一处可能影响全局,加一段工具描述可能跟前面的指令冲突
- 每次请求都带全部内容,即使当前对话用不到某些段落也浪费 token,还打不中 prompt cache
核心洞察 :System prompt 不是"文档",而是运行时根据当前状态组装的配置。哪些工具启用、哪些 skill 可见、哪些记忆相关、哪些内容必须保持稳定 ------ 都应该在运行时决定。
System prompt 的组成结构
一个合理的 System prompt 通常包含以下几层(从稳定到易变):
| 层 | 内容 | 变化频率 | 是否命中 cache |
|---|---|---|---|
| 身份 | Agent 的角色定位、行为准则 | 几乎不变 | ✓ 强 cache |
| Tools | 当前启用的工具 schema | 按会话变化 | ✓ 会话内 cache |
| Skill catalog | 可用 skill 的索引(name + 一句话描述) | 按会话变化 | ✓ 会话内 cache |
| Memory catalog | 长期记忆的索引 / 摘要 | 按会话变化 | ✓ 会话内 cache |
| Workspace | 当前工作目录、项目上下文 | 按任务变化 | ✗ 每次变 |
排列顺序至关重要 ------ 越稳定的内容放越前面。 Claude、GPT 等主流 LLM 都支持 prompt caching:相同的前缀可以复用,只对增量部分收费。把 workspace 放到最后,前面几层就能最大化 cache 命中率,直接降低成本和延迟。
Java 程序员视角:这类似 Spring 的 bean 生命周期 ------ 身份是 singleton(启动时构造一次),Tools / Skill catalog / Memory catalog 是 session-scoped(按会话构造),Workspace 是 request-scoped(每次请求构造)。层次越稳定,复用越充分。
与前面章节的呼应
System prompt 是前几章"分层加载"机制的汇聚点:
- Tools schema ------ 对应 Tools 管理章节的"需要将支持的 Tools 告诉 LLM"
- Skill catalog ------ 对应 Skill 章节的"启动时只加载 name + 一句话描述"
- Memory catalog ------ 对应 Memory 章节的"catalog 常驻 system prompt,全文按需加载"
所有 catalog(能力目录)在 System prompt 汇总,而"详情"(skill 全文、memory 全文、tool 执行结果)通过按需加载注入 MessageList。
一句话总结:System prompt 定义"你是谁、你能做什么、你在哪里工作",MessageList 记录"你正在做什么"。前者相对稳定用于命中 cache,后者动态生长承载具体任务。
未完待续。