
一个裸的 AI 编程 Agent 是什么?一颗泡在罐子里的大脑。它能思考,能生成代码,能调用函数——但它没法在凌晨三点回你的 Slack DM,没法重试一个挂掉的 CI Job,没法修那个刚出现在它 MR 上的 merge conflict,也记不住昨天 reviewer 问的问题还没回复。
让这颗大脑真正有用的东西,叫 Harness:套在 LLM 外面的运行时脚手架,给它装上感官、双手和记忆。事件接入、Agent 编排、持久化状态、自愈循环、可观测性、人机协同控制面——这些才是让 LLM 从"会聊天的模型"变成"能干活儿的系统"的东西。
我们搭了一套,跑在 Claude Code 上面,已经连续运转了几个月。从 Slack 收信号,往五个内部仓库开 MR,半夜回 reviewer 的评论,CI 挂了悄悄自己修。这篇文章就是这套系统的自顶向下拆解:它做什么、各组件怎么拼、以及那些只有被生产毒打过才会有的设计伤疤。
2023 年 ChatGPT 插件刚出来的时候,很多团队干了一件特别自然的事:在 LLM 前面放个聊天 UI,挂几个 function-call 工具,然后宣布"我们有了工程 Agent"。
这模式在 demo 里几乎能跑,在生产里从来跑不了。第一周就会出现三种死法:
解决这些问题是 Harness 层的活儿。Harness 是把 claude.ai 的一个聊天标签页,和 Claude Code 本身、Cursor 的后台 Agent、Cognition 的 Devin、或者我们搭的这套系统区分开来的东西。大脑(模型)在这些系统之间基本可以互换——变的是套在外面的 Harness。
大脑只通过 Harness 跟世界对话。让 LLM 表现得像个系统的,几乎全是 Harness 的工程活儿。
起点是一个具体问题:一个 Slack 频道——就叫它 #support——每周攒大概 30 张工单,每张都需要排查,然后(通常)在五个仓库之一里改代码。每张工单跨好几天。Reviewer 在 MR 上留评论。CI 抽风。有时候排查到最后发现跟上个月的是同一类问题。
Harness 有三层职责,按介入深度排:
#support 里出现新工单,跟踪 thread,跨仓库收集上下文,在原 thread 里贴一份结构化分析。第三层职责是这套系统跟一个花哨工单机器人的本质区别。Harness 的基板是它自己的源码,LLM 通过人工 review 的 PR 有写入权限。
Harness 围绕一个洞察搭建:一个无状态的、被动反应的 LLM 不是生产系统。六层架构的存在就是为了堵三个窟窿:
窟窿 | 对应层 | 机制 |
|---|---|---|
反应性——LLM 自己不会醒 | Layer 1: Event Ingestion | Slack 提及、GitLab CI 结果、PagerDuty 告警 → 统一调度队列 |
持久性——LLM 会话间失忆 | Layer 3: Persistent State | 内存去重、本地 JSON 运营映射、git 同步工作区 |
质量——LLM 从弱证据写自信结论 | Layer 2: Agent Orchestration | 结构化输出契约、质量门禁、对抗性审查、自愈循环 |
把这些层串起来的是复利效应:每关闭一个 case,下一个就更快。一份被批准的排查分析变成知识库条目。一个被批准的 MR 变成 gap-analyzer 可以引用的模式。一个完结的 case 对 Harness 自身开一个改进 MR。系统一轮一轮地缩小它处理不好的工单类别。
Harness 需要被三种外部信号唤醒:
每种信号的延迟和可靠性特征不同,所以用了不同的入口。
最终一致性。 Slack 的 conversations.history 端点可能滞后 1-30 秒。如果我们的 poller 看到空结果就天真地推进 cursor,任何 ts 落在这个窗口里的消息就会被永久跳过。我们的做法是:空轮询时冻结 cursor,从同一个最老时间戳重试,直到至少回来一条消息。跨重试的重复处理用 per-message-ts 去重集合过滤。
Socket Mode + poller 双保险。 Socket Mode 给我们亚秒级延迟处理 thread 回复。但 WebSocket 连接会断。Poller 是安全网——它用共享的 socket-dedup 文件捕获 Socket Mode 漏掉的一切,同一条消息不会派发两次。
MR 监控的扩展性问题。 随着 Harness 接管的长期 MR 越来越多,它 10 分钟一轮的轮询循环会累积死 thread cursor,每个 cursor 每轮消耗一次 Slack API 调用。不处理的话,持续产生 429 限流风暴。我们加了自动驱逐:当上游 API 返回 thread_not_found 时清理 cursor。
这是 LLM 真正运行的地方。Harness 把 worker 作为临时 systemd 单元启动,每个带着一个 JSON payload,包含工作流名称 + case_dir + thread 上下文。
每条 pipeline 是一次长运行的 Claude Code 调用,带着精心编排的 prompt 和一个可写的 case_dir。case_dir 是 Agent 的草稿空间:里面有 case 的 TASK.md、累积的 followup_transcript_*.md 文件、生成的 gap_report*.md 文件、以及中间 JSON 产物。
我们没有一个大而全的 Agent——而是几个专项 Agent,每个在明确的交接点串联:
Agent | 角色 |
|---|---|
oncall_run | 初始排查——最多 20 轮推理迭代,质量门禁把关,带显式自我批判。输出:ANALYSIS.md + TASK.md |
case_followup | 长生命周期聊天 Agent。操作者可以 DM 回复好几天;每次回复用完整对话历史重新调用。每 N 条回复还会调 gap_analyzer 作为子 Agent |
finalize_case | 收尾者。case 闲置 4 小时后,收集所有 gap_report,过滤误报,对 Harness 自身代码库开一个 MR 带上存活的 gap 修复 |
dev-agent | 干活的。在 per-case 的 git worktree 里跑,改代码、push、盯 CI、CI 挂了就修、回 reviewer 评论 |
两个不那么显然的架构选择:
case_dir 是唯一事实源。~/.harness/ 里的状态文件映射 Slack thread → case_dir。Agent 始终相对 case_dir 操作。"这玩意儿到底干什么"一句话回答:操作者在 Slack 里敲一条命令,一个迭代推理、自我批判、工具使用的闭环就跑起来了,不再需要人工干预,直到活儿干完或者系统诚实承认搞不定。
在第一次 Claude 调用触发之前,Harness 让 Claude 承诺一个 hypothesis_slate:至少六个候选解释,外加四到五个评估标准用来从中选择。这些被冻结在 kickoff_precommitment.json 里,在后续排查中视为不可变。
这是整套系统里最重要的方法论保障。没有它,LLM 推理循环会向早期证据最支持的假设收敛,然后回顾性地把所有后续证据框定为对它的确认。预承诺锁强制排查从一个完整的假设空间出发,并记录从这个空间到结论的路径。
只在第一轮迭代,Harness 跑一组 OS 级命令,LLM 不参与——journalctl 错误尾部、磁盘和内存快照、日志文件发现。输出作为原始证据注入第一个排查 prompt。这让 Claude 在形成任何假设之前,先拿到一份事实性的系统状态快照。
每轮迭代以一个可解析的 JSON 契约结束——completion_report:
{
"status": "IN_PROGRESS" | "BLOCKED" | "COMPLETE",
"confidence": 78,
"open_questions": ["..."],
"unchecked_sources": [{"name": "...", "access_status": "..."}],
"contradiction_register": ["..."],
"assumption_register": ["..."],
"adjacent_problems": [{"summary": "...", "status": "...", "blocker": "..."}],
"draft_response": "面向用户的 Slack 消息"
}
这个块前后允许自由文本,但门禁评估的是解析后的报告。这是让 LLM 输出在 pipeline 中可用的最大杠杆点:用一些散文自由度换一个可解析的契约。
每轮 Claude 之后跑三族门禁:
G-gates(逻辑一致性)——强制推理不跨轮自相矛盾。G1:如果本轮 open_questions 增加了但 confidence 上升了——分母扩大,任何上升都无效。共 4 个 gate。
N-gates(结构完整性)——检查排查产物是否完整。N1:每个可执行的修复动作必须配对 verify_action,带描述、预期结果和时间。共 3 个 gate。
A-gates(断言上限)——施加 Claude 无法通过散文绕过的机械限制。A1 计算硬置信度上限:1.0 − (open_questions × 0.08) − (unchecked_sources × 0.05)。共 7 个 gate。
外加两个抽查:EQ1 要求根因节点引用硬证据(非 INFERRED),否则接受 60% 上限。P7 对每个标记 FAIL 的对抗性维度降 7% 上限,地板 40%。
Gate 违规变成 pending_guard_notes,预置到下一轮 prompt 的最前面。指令很明确:处理完所有 notes 再继续。这是强制机制——gate 不阻止 Claude;它们让 Claude 上一轮的违规成为下一轮它读到的第一件事。
Agent 每轮自评一个 confidence:0-100 的整数。这个数字门控真实行为:
degrading 原因退出。完整 confidence 轨迹写入 ANALYSIS.md 作为 sparkline:45% → 62% → 78% → 80%。操作者看这条 sparkline 来判断该不该信这个结果。
causal_chain_complete != true——症状级发现,没追溯到根因。一旦排查宣称完成且 confidence ≥ 70,控制权交给一个独立的 Claude 调用,扮演对抗性审查者角色。审查者拿到分析文本 + 一份 15 个评估维度的显式清单(D1-D15:假设有效性、证据链、因果推理、替代假设等)。
三种退出条件:
pending_guard_notes,循环重启还有一个结构上不同的红队审查(G5)。红队只看到原始问题陈述和最终结论——没有中间推理,没有证据轨迹。它产出一个独立假设,标记排查发现和冷读者预测之间的差距。目的是认知隔离,不是覆盖率。
exit=complete confidence=89% iterations=12 elapsed=13min
exit=blocked confidence=62% iterations=4 elapsed=4min
exit=stalled confidence=58% iterations=7 elapsed=22min
exit=degrading confidence=42% iterations=5 elapsed=8min
exit=timeout confidence=73% iterations=18 elapsed=50min
每条 pipeline 启动一个 Claude Code 子进程,带 --permission-mode bypassPermissions。MCP 目录覆盖五个外部系统:Slack、Datadog、Jira/Confluence、Glean、GitLab。
关键约束:
draft_response两个循环在不同时间尺度上运行:
Loop 1 — Per-case 结构修复(gap → PR)。 每次排查关闭后,gap_analyzer 识别 Harness 哪里表现不佳。case 闲置四小时后,finalize_case 在专用 worktree 里启动 patch_suggester。结果是一个针对 Harness 自身代码库的 PR——编辑代码文件、prompt 模板或 SOP 段落。PR 需要人工 merge,不需要人工写。
Loop 2 — 跨 case 行为强化(failure mode → SOP 建议)。 每个 case 的 auto_retrospective 编目哪些 failure mode 触发了。每周 cron 统计最近十个 case 的频率,当任何 mode 跨过阈值时生成 SOP 建议:10 个里 3 个触发建议,6 个触发告警。
这就是让学习持久的机制。上下文窗口活一个会话。一个合并的代码变更是版本化的、审查过的、测试过的、永久的。基板在进化;下一个 case 跑在比上一个更好的 Harness 上。
状态存在三层:
层 | 内容 | 特征 |
|---|---|---|
内存缓存 | Poller/keepalive 进程状态 | 极小,重启可重建 |
本地状态 (~/.harness/*.json) | 运营映射:thread→case_dir、MR→CI 状态、kill switch 标志 | 重启安全,机器本地 |
Git 同步状态 | Case 工作区提交到私有仓库 | 持久,跨机器可恢复 |
操作者从两台机器工作:一台笔记本和一台长期运行的 CVM。同一个 Claude 会话需要从任意一台恢复。Harness 用 git 支持的会话存储解决这个问题:
.jsonl + .meta 文件在每次有趣的状态变更时提交并推送。.meta 文件,找 CWD 匹配的那个,恢复对应 UUID。同步 hook 做三个操作,严格顺序,通过 flock 串行化:
长对话积累几十个 followup_transcript_*.md 文件。一个子 Agent 把它们全读了,重写 TASK.md 包含蒸馏后的状态。合并在四个信号上触发:transcript 中出现 commit SHA 或决策动词、刚关闭的 review thread、case 工作区累积 transcript 超过 5KB、或距离上次合并至少一小时。
三个循环,每个都在没有操作者干预的情况下闭合:
每个可能失败的原始操作(push、rebase、MR 评论、branch reset)都坐在三层恢复架构上:
层 | 机制 | 特征 |
|---|---|---|
L1 — 确定性守卫 | 基于规则,亚秒级,始终在线 | _pre_push_rebase:每次 push 前 fetch + rebase。小到可以推理,有界到可以在每次操作上运行而不消耗 token |
L2 — Agentic 自愈循环 | 受限 Claude 会话 | ≤3 次尝试,每轮 4 分钟超时,confidence gate ≥ 70。Claude 对成功的信念从不被信任——git push exit 0 才是 ground truth |
L3 — 操作者升级 | 完整 L2 轨迹 DM 给操作者 | 每次尝试、每个动作、每个 confidence 评分、最终 git 状态 |
四个可观测面:
~/.harness/logs/agent.log 和 error.log;每次 spawn、chain dispatch、重试尝试~/.harness/logs/system.log 用于跨切面事件我们做了一个明确的选择:不建 Dashboard。 Slack 和 grep 就是控制台。Dashboard 增加第三个需要维护的面,而且是工程师在它变陈旧后第一个停止看的东西。
一个小服务——mcp-watchdog——每 10 分钟轮询每个 MCP 的健康端点。如果失败:尝试静默 token 刷新,刷新失败发 Slack DM,连续两次断连后升级。没有这个循环,一个过期的 token 表现出来的是"Agent 今天怎么这么蠢"——这类故障诊断起来极其痛苦,因为 agent.log 里没有任何东西说"token 过期了"。
所有操作者交互走单一通道——self-DM——组织成六个命令族:
命令族 | 示例 |
|---|---|
Task | admin: task <jira>,--auto 无人值守执行 |
On-call | admin: oncall-run <url>,oncall-toggle |
MR 控制 | admin: mr rescan,mr pause/resume |
Pause | admin: oncall pause [duration],oncall resume/status |
chat.postMessage 验证目标频道是否在白名单内。唯一允许的目标是操作者的 self-DM。如果 Agent 试图直接给队友发消息,必须把内容弹回给操作者。Harness 目前服务两个项目,每个有不同的运营规则。加第三个需要零代码改动——只需在 agent-config/projects.yaml 里加一条:
projects:
team-a:
sop:"team-a/SOP.md"
kb:"team-a/KNOWLEDGE-BASE.md"
slack_channels:["support-channel-a","support-channel-b"]
jira_prefixes:["AAA","BBB","CCC"]
team-b:
sop:"team-b/SOP.md"
kb:"team-b/knowledge-base/kb.md"
slack_channels:[]
jira_prefixes:["XX","YY"]
resolve_project() 函数按优先级路由每个入站信号:Jira 前缀 > Slack 频道 > 默认。每个项目有自己的 SOP 文件、KB 文件和目录树。
原始设计是 Agent 产出一个 diff,Harness 在主仓库 checkout 里调 git apply。这有两种失败模式:
git apply --check 中途失败,主仓库会留在半应用状态修复方案是 worktree_manager 服务。在任何需要改代码的 Agent 运行之前,manager 在 ~/.worktrees/case-<case_id>-<stamp>/ 创建一个沙箱 worktree,从干净分支 checkout。Claude 在那个 worktree 里用它的原生 Edit/Write 工具操作——没有 git apply,没有 diff 文本往返。MR 合并或关闭后 worktree 被拆除。
Harness 的源码住在一个私有 GitHub 仓库里。当 gap-report → finalize → MR 循环产出一个针对 Harness 自身的改进 MR 时,那个 MR 有自己的 CI。如果 Harness 改进 MR 的 CI 挂了,一个单独的服务——pr-ci-fixer——用和 mr-monitor 处理客户 MR 失败一样的方式捡起来。
结果:Harness 不仅自愈它为工单写的代码,也自愈它为自己写的代码。
层 | 测什么 | 例子 |
|---|---|---|
L0(纯边界) | 无 mock、无文件系统、无 IO。纯逻辑——gate 函数、JSON 提取、confidence 上限数学 | Gate 算术正确性 |
L1(文件隔离) | tmpdir 里真实文件系统,真实线程 | 并发写竞态、状态文件原子性 |
L2(服务隔离) | 真实子进程执行 + mock 外部 API | 调度路由、payload schema |
L3(沙箱) | 真实 git 仓库,假 Slack 和 Claude 响应 | 完整工作流序列 |
L4(集成矩阵) | Actor 契约、admin×role 矩阵、跨运行状态一致性 | 服务间链路 |
L4g(生产回放) | 13 个测试把特定生产事故复现为回归测试 | 编码化的机构记忆 |
Harness 开的每个 MR 都经过一套风格 SOP,目的是让输出不像 AI 写的:
Co-Authored-By: Claude,没有 Generated with Claude Code,没有 [bot] 后缀**根因:** 后跟一段话被禁pytest 命令带输出一张真实工单到达时发生了什么:
T+0min Slack 消息出现在 #support
T+1min Harness 捡起来,启动 oncall_run
T+5min 排查循环开始(预承诺锁 + 经验锚点)
T+15min 第一份 confidence 报告贴到 thread
T+20min 对抗性审查触发(独立 Claude 调用)
T+25min 操作者审查 ANALYSIS.md,批准
T+30min dev-agent 在隔离 worktree 里启动,改代码
T+35min MR 打开,CI 跑起来
T+40min CI 挂了 → 自动修复循环(L1 确定性 → L2 agentic → L3 升级)
T+50min Reviewer 评论 → 自动回复循环处理
T+60min CI 绿了,reviewer 批准,MR 合并
T+90min Case 完结,gap_analyzer 跑,自我进化循环反哺
端到端,这个循环通常在 30-90 分钟墙钟时间内完成。操作者的总主动参与时间通常在 5 分钟以内。
我们异常深的地方:
我们刻意薄的地方:
Harness 还在进化——自治循环的覆盖缺口、成本计量、正式 eval harness——没有一个是阻塞性的。系统已经在做它被造出来要做的事:把 Slack 信号转成合并的代码,半夜回 reviewer 反馈,自愈 CI,把自己的改进反哺到下一个版本的自己。
更深的教训是 meta-loop。Harness 里的每一道伤疤都是因为一张真实工单踩出来的。构建循环和运营循环是同一个循环,只是时间尺度不同。生产信号流回 Harness 自己的源码;Agent 提出修复;人类审查并合并;下一类 bug 离被修剪又近了一步。
先建循环。再让它自治。顺序很重要,因为一个没有反馈路径进入自身基板的自治系统,只是一个更快的方式,把同一组错误规模化地交付出去。