犀利豆的博客

高质量的技术博客

AI in Harness(一)

最近系统性地研究了一下 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 基础上增加了上下文工程、工具设计、权限管控、记忆管理、上下文压缩等能力。

loop

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 -lgrep 等只读操作
  • 需要询问的git commitgit 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 的机制:

  1. 启动时只加载 skill 的 name + 一句话描述(占用极小)
  2. 用户发起请求,LLM 判断需要哪个 skill
  3. Harness 动态读取对应 skill 的 markdown 全文,注入 context
  4. LLM 按 skill 里的步骤,调用底层 tool 完成任务

Skill 与 Tool 的层次关系:

1
2
3
4
5
6
7
 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 总结,提炼上下文内容。

核心思路:

  1. 先基于规则,使用便宜的方式
  2. 压缩可回溯,需要时再查询
  3. 最后才用 LLM 总结

Memory - 让 Agent 拥有记忆

在 Loop 中随着信息不断添加与压缩,我们还需要设计一个机制,保存需要记住的东西。

Memory = 把对话里值得永久记住的事实(fact)落到磁盘文件,跨 Compact 不丢、跨 session 仍可用。

Memory 的核心模块:

1
2
3
4
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 长大后,会遇到三个问题:

  1. 换项目要重写整个 prompt,不知道哪些该改、哪些该留(身份、工具、规范混在一起,牵一发动全身)
  2. 修改一处可能影响全局,加一段工具描述可能跟前面的指令冲突
  3. 每次请求都带全部内容,即使当前对话用不到某些段落也浪费 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,后者动态生长承载具体任务。

未完待续。

负载均衡

前端

使用 DNS 进行负载均衡。在 DNS 回复中提供多个 A 记录或者 AAAA 记录。
虽然 DNS 看起来简单,但是存在不少问题。

  1. DNS 对客户端行为的约束很弱:记录是随机选择的。
  2. 客户端无法识别“最近”的地址
  3. 权威服务器不能主动清楚某个解析器的缓存,DNS 记录需要保持一个相对低的失效值(TTL)。
Read more »

事后总结:从失败中学习

哲学

保证事故能够被记录下来,理清所有根源问题。确保实施有效的措施是的未来重现的几率和影响得以降低,甚至避免。

书写事后总结不是一种惩罚,而是整个公司的一次学习机会。

需要书写的标准:

  • 用户可见的宕机或者服务质量下降到一定标准
  • 任何形式的数据丢失
  • on-call 工程师需要人工介入
  • 问题解决耗时超过一定限制
  • 监控问题
Read more »

应急事件响应

测试导致的事故

SRE 故意破坏系统,利用这些测试发现系统的薄弱地方。

在某次测试中发现了额外的系统依赖。

响应

  • 终止测试
  • 用以前 测试过的方法 回滚了数据
  • 找到开发者修复了相关问题
  • 制定了周期性测试机制来保证问题不重现

事后总结

好的方面:
事先沟通,有足够信息推测是测试造成的问题。
快速恢复了系统。
遗留一个代办,彻底修复问题。制定了周期性的测试流程。

Read more »

有效的故障排查手段

理论:

反复采用假设排除手段的过程:
不断提出一个造成系统问题的假设,进而针对这些假设进行测试和排除

常见的陷阱

  • 关注的错误的系统现象,或者错误地理解了系统现象的含义。
  • 不能正确的修改系统的配置信息,输入信息或者系统运行环境。
  • 将问题过早的归结为极为不可能的因素,或者之前曾经发生过的问题
  • 试图解决与当前问题相关的一些问题,却没有认识到只是巧合。
Read more »

新财年换了领导,管理风格也有一些区别。在团队内增加了一个 SRE 的职位。这一财年我将会承担一部分 SRE 的工作。

之前作为开发者,总的来说从开发的角度来思考系统的稳定性。现在需要从更高更全面的角度来思考和理解站点的稳定性。上网研究了一番,SRE 是 google 的一个职位同时 SRE 也是一套 google 总结出来的站点稳定性的方法论。所以找来了 《SRE google 运维解密》。这本书成书比较早,里面有些章节介绍的技术栈可能过时。具体我也不了解 google 内部是否还在使用。但是方法论还是很合理、科学的。

一直以来我工作过的团队对于风险的态度都是,预防和杜绝。但是在这本书里面,google 对于风险的态度就变成了管理,合理使用,甚至利用风险来保证项目的迭代。

Read more »

2021 就这么结束了。

今年我做爸爸了。
今年九月,迎来了我们家的小朋友。豆嫂从怀孕一路走来。如打怪升级一样。一关一关的过,颇为不容易。

jia

Read more »

终于有一个 Java 版的微信机器人了。

公众号很久没有更新了。主要两个原因,换了工作之后,第一,要花更多的时间去了解和学习新的业务。第二,我最近把几乎所有的业余时间都来写这个 Java 版的微信机器人了。

java-wechaty

Wechaty 是什么

官网的描述是:

  • A Conversational AI RPA SDK for Chatbot

其实就是一个能够快速构建聊天机器人的开源 SDK。最早的时候,Wechaty 只是一个基于服务于微信工具库,现在逐渐的发展到可以对接世面上的主流聊天软件包括不限于:微信,企业微信,钉钉,Line 等。

编程语言也由原来的单一语言(TypeScript) 发展到,Java,Scala,Python,Go 等多语言实现的工具库了,同时社区生态还在不断的壮大。

Github 地址:https://github.com/wechaty/wechaty 目前已经有 7.9k 的 star 了。

Read more »

最近研究 Vetrx 简直爱不释手。迫不及待的想给大家介绍一下。

carbon

Vertx 是什么

  • Vertx 是一个运行在 JVM 上,用来构建响应式应用的工具集。
  • 基于 netty 的高性能的,异步的网络库。
  • 对 netty 进行了封装,提供更加友好的 API。
  • 同时实现了一些基于异步调用的库,包括database connection, monitoring, authentication, logging, service discovery, clustering support, etc。
Read more »
0%