ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Day65|从0学习 Claude Code(十五):Harness 集成,14 章的零件装回同一台车

Day65|从0学习 Claude Code(十五):Harness 集成,14 章的零件装回同一台车 苦猿的大模型日记 · Day65 · 从0学习Claude Code十五Harness集成-帮普通人把AI学进简历系列前言零件全有了车还跑不起来本篇干一件事不引入任何一个新机制把前面攒下的所有零件——工具分发、权限、Hooks、todo、子 agent、技能、记忆、上下文压缩、后台任务、定时调度、团队、worktree、MCP——全部装回同一个while True。先说实话这是整个系列最难的一篇。难的不是新知识是集成。那些零件单独都会跑权限示例能拦命令记忆示例能存偏好定时示例能到点触发。可真把它们倒进同一个工程问题立刻排队上门cron 到点的提醒怎么送进正在跑的对话后台任务完成了谁来告诉 Agent队友发来的 plan排在用户问题前面还是后面上下文压缩和记忆注入谁先谁后用户正在敲键盘定时任务到点了两轮对话撞车怎么办每一个单拎出来都不难难的是它们共享同一段对话、同一个工具池、同一份上下文预算——动任何一个其他全部跟着晃。读完这篇你会拿到三样东西一张全景图十几种机制各自挂在循环的哪个位置逐段读懂主循环的能力一轮从进门到出门到底发生了什么事件驱动机制Agent 跑完一轮就睡了是谁把它叫醒的门槛不变一路跟到现在的读者直接开跑新来的朋友只需要记住一句话——Agent 循环 调模型 → 看响应里有没有工具调用 → 有就执行、结果回消息。PART 01集成第一决策——循环一个都不多先看最反直觉的地方。这个集成版的 Agent工具从最早的 5 个涨到了26 个机制从 0 涨到了 13 种但主循环的结构一行没变调用模型 → 响应里有 tool_use block 没有 → 本轮结束 有 → 执行工具 → 结果追加回 messages → 下一轮还是那三步。这不是偷懒是整个集成方案里最重要的一条纪律循环结构不许动。为什么因为循环是全系统的主干道。每加一个机制就改一次循环改到第十次循环会变成一坨谁也不敢碰的面条——每个机制都认识其他所有机制牵一发动全身。正确的做法反过来循环不认识任何机制机制自己找地方挂。于是整个系统只有四类挂点LLM 前定时任务、后台通知、压缩管线、记忆和技能组装——把该带的行李塞进上下文工具执行前PreToolUse Hooks 权限——把不该执行的动作拦下来工具执行后PostToolUse Hooks——大输出告警、日志这些后处理本轮收尾没有 tool_use、循环要退出时Stop Hooks 做统计和清理十几种机制四类挂点。就像装修房子不用砸承重墙——水管走水管的位置电线走电线的位置房子还是那个房子。机制可以无限加循环永远是那一个。这是集成全部设计的前提。PART 02主循环逐段拆解——一轮到底发生了什么把主循环的代码简化后摆出来这是全篇的主角def agent_loop(messages, context, active_request): tools, handlers assemble_tool_pool() while True: # ① 到点的定时任务作为 user 消息注入对话 for job in consume_cron_queue(): messages.append({role: user, content: f[Scheduled] {job.prompt}}) # ② 后台任务完成的通知同样注入对话 inject_background_notifications(messages) # ③ 连续三轮没碰 todo提醒它更新计划 if rounds_since_todo 3: messages.append({role: user, content: reminderUpdate your todos./reminder}) # ④ 压缩管线 组装 system prompt 和工具池 prepare_context(messages, active_request) context update_context(context, messages) tools, handlers assemble_tool_pool() # ⑤ 调模型外面包着错误恢复 response call_llm(messages, context, tools, state, max_tokens) messages.append({role: assistant, content: response.content}) if not has_tool_use(response.content): trigger_hooks(Stop, messages) # 没有 tool_use本轮结束 remember_after_turn(messages) # 提取记忆 return # ⑥ 逐个执行工具结果回 messages results [] for block in response.content: if block.type ! tool_use: continue blocked trigger_hooks(PreToolUse, block) # 权限在这拦 if blocked: results.append(tool_result(block.id, blocked)) continue output call_tool_handler(handlers[block.name], block.input) trigger_hooks(PostToolUse, block, output) results.append(tool_result(block.id, output)) messages.append({role: user, content: results})逐段看。①② 是全篇最值钱的一个设计cron 到点的提醒、后台任务的完成通知、队友发来的消息——统统变成messages里一条role: user的内容。不管事件从哪来定时器、后台线程、队友收件箱进入对话的姿势只有一种变成一条用户侧消息。对模型来说它看到的永远只是一段对话根本不知道也不需要知道这条消息背后是哪个机制产生的。我把这叫统一货币所有事件先兑换成 messages再进循环。没有cron 专用通道没有后台任务专用格式。谁想给模型递话谁就学会说 messages 这一种语言。③ 是防漂移的细节。Agent 干活一上头三四个工具轮过去早把最初的任务清单忘干净了越干越歪。这里连续三轮没碰todo_write就插一条提醒。成本几乎为零效果是把跑偏的 Agent 拽回来。④ 是上下文的账房先生。调模型前先过四级压缩管线tool_result_budget → snip_compact → micro_compact → compact_history先给大块工具输出套预算再裁中段超限了才动摘要——能裁就不删能删才总结每一步都比下一步便宜。而且裁剪手下有分寸micro_compact只在真超限时才动手动手也只替换较早且已经读过的结果最近 3 条消息永远保持完整逼近阈值的 80% 就停手——宁可这轮少省一点也不让模型突然失忆忘了自己刚干了什么。同一时刻记忆、技能目录、已连接的 MCP server 也组装进 system prompt工具池现场重新拼一次——上一轮刚连上的 MCP server这一轮的工具就位。⑤ 外面那层错误恢复上一批就位了429 退避重试、529 切备用模型、max_tokens先升档再要续写、prompt 太长触发应急压缩。集成版没加新东西只是确认它们在总装车上还灵。⑥ 是三岔路口。每个tool_useblock 走到这面临三种命运被 PreToolUse 拦下返回拒绝理由也算一种 tool_result标记了后台执行的 bash拿占位符走人真结果以后以通知形式回来正常前台执行。三条路殊途同归——都变成 tool_result都回 messages。你看又是统一货币。事件进来是货币结果出去还是货币。整个系统里只有一种数据在流动。PART 03权限不是一层 if是一个挂点总装车上最容易被低估的是权限的位置。直觉写法是把权限写成工具执行旁边的一层 ifif 危险: 拦下。单文件示例这么写没问题集成版这么写会漏——你自己写的工具拦住了那队友调用的工具呢一次性 subagent 调用的呢MCP 插进来的外部工具呢而且集成版的工具池本身就不固定。每轮调模型前assemble_tool_pool()现场把两路工具合进一个池子def assemble_tool_pool(): tools list(BUILTIN_TOOLS) # 26 个内置工具 handlers dict(BUILTIN_HANDLERS) for server_name, client in mcp_clients.items(): for tool_def in client.tools: prefixed fmcp__{server_name}__{tool_def[name]} if prefixed in origins: # 规范化后撞名当场炸 raise ValueError( fMCP tool name collision: {prefixed!r}) tools.append(to_mcp_schema(tool_def)) handlers[prefixed] client.call_tool return tools, handlers内置 26 个打底MCP server 连几个就动态插几个connect_mcp(docs)之后下一轮池子里就多出mcp__docs__search。注意那个撞名检查两个 server 的工具规范化之后名字撞了当场抛异常绝不悄悄合并——两个同名工具悄悄变成一个调用路由到错误的 server这种 bug 出了根本查不到。池子是动态的权限更不能绑死在某个工具的实现里。集成版的答案权限不写在执行处写成 PreToolUse Hook。看简化后的代码def permission_hook(block): if block.name bash: command block.input.get(command, ) for pattern in DENY_LIST: # rm -rf /、sudo、mkfs 这些 if pattern in command: return fPermission denied: {pattern} is on the deny list if threading.current_thread() is not threading.main_thread(): return (Permission denied: interactive shell approval is unavailable during an asynchronous turn) if CONSOLE.ask( Allow? [y/N] ) not in (y, yes): return Permission denied by user if block.name in (read_file, write_file, edit_file): path block.input.get(path, ) if not (WORKDIR / path).resolve().is_relative_to(WORKDIR): return Permission denied: path is outside the workspace if block.name.startswith(mcp__) and \ mcp_tool_policies.get(block.name, confirm) ! allow: if CONSOLE.ask( Allow? [y/N] ) not in (y, yes): return Permission denied by user return None # None 放行这么挂之后主循环里只剩一句trigger_hooks(PreToolUse, block)。Lead 的工具、subagent 的工具、队友线程的工具全部从这道门过——因为门挂在主干道上没有旁路可绕。而且 Hook 是可以排队的permission 挂一个日志再挂一个审计再挂一个互不认识各司其职。权限从工具执行的一部分升级成了主干道上的独立检查站。这段代码里还藏着两处容易被忽略的工程品味。第一处MCP 工具的默认值是confirm。宿主自己维护一份精确的已知只读工具名单比如mcp__docs__search名单之外的 MCP 工具一律问用户。为什么不让 server 的 description 说了算因为 description 是 server 自己写的——自我介绍不能当授权书。一个标着只读查询的工具谁知道它背后干了什么。第二处那句线程检查。threading.current_thread() is not threading.main_thread()——非主线程直接拒绝需要交互确认的操作。为什么因为异步轮次下面马上讲可能发生在用户正在终端里敲字的时候这时候弹一个Allow? [y/N]出来会跟用户正在输入的内容抢终端两边都是灾难。所以宁可拒绝不抢输入。对危险动作宁可错过不可错放对用户终端宁可拒绝不可打扰。PART 04睡着的 Agent谁来叫醒前面留了个坑主循环跑完一轮就return了Agent 就此睡着。那第二天早上九点的定时任务谁来执行后台装依赖装完了谁来接着干活答案是集成版真正的第二个循环——事件循环def async_event_loop(history, context, session_state): while True: time.sleep(1) with agent_lock: # 和用户输入抢同一把锁 fired list(cron_queue) # 事件源①到点的定时任务 inbox consume_lead_inbox() # 事件源②队友消息/plan/关机 if not fired and not inbox \ and not has_pending_background(): continue # 事件源③后台任务。全空接着睡 ... agent_loop(history, context, active_request)这个后台线程每秒醒一次检查三个事件源cron 队列、Lead 收件箱、已完成的后台任务。三个全空接着睡任何一个有货就叫醒一轮agent_loop。注意那把agent_lock。用户在前台敲一个问题会触发一轮循环事件循环也可能同时想触发一轮——两轮对话操作的是同一份 history并发跑必然写花。所以两边抢同一把锁用户在用事件循环排队等事件循环在跑用户输入等它跑完。两个循环一份历史一把锁。我拿两个实验跑过它建议你也试试。实验一3 分钟后提醒我开会然后什么都别干去泡杯茶。到点你会看到一条[cron auto]日志Agent 自己醒来给自己安排了提醒——全程没人碰键盘。实验二在后台安装依赖同时继续阅读 README.md。bash 被标记成后台执行主循环立刻拿到占位结果继续读文件依赖装完完成通知以事件形式唤醒下一轮。两件事在时间上重叠在对话里串行——模型看到的永远是一段有条不紊的历史。最后是 cron 的交付语义这里有个容易漏想的细节到点的任务是先攒在unacknowledged_cron_jobs里等包含这条 prompt 的模型调用成功才确认消费失败就塞回队列重启后也会重新入队。翻译成人话定时任务的通知是至少送达一次。宁可重复提醒你一次开会绝不悄悄吞掉一次提醒。对一个没人值守的调度系统吞任务是最恶劣的故障——重复只是烦丢失是事故。结尾从裸循环到一台完整的 Harness回顾这一篇的三块核心循环不动结构一行不改机制自己找挂点主干道不认识任何一个机制、统一货币cron、后台、队友、工具结果统统兑换成 messages 再进对话、事件驱动主循环睡了事件循环每秒查三个源拿同一把锁唤醒它。再退远看一眼这辆车26 个工具、长期记忆、自动压缩、任务图、队友线程、定时调度、可插拔的外部服务。它的第一个形态是几十行代码、5 个工具、跑两轮就把上下文塞爆的裸循环。十四篇每篇只加一个机制这一篇把它们装回同一台车——循环本身还是第一天那三步。循环只是心跳挂点才是器官——Agent 的强大从来不在于跳了多少轮而在于每一轮前后长出了什么。互动时间如果让你往这个循环上再挂一个机制你会挂什么我先来一个预算 Hook每次调模型前算一遍这轮的 token 成本超线就拦。评论区说说你的答案。下一篇预告「从0学习 Claude Code」第十六篇——Workflow Runtime。车装好了下一站解决新问题有些活儿的步骤是固定的凭什么每次都让模型现场发挥把编排路径写进代码、记录运行进度、中断了还能接着跑——工作流运行时给这台 Harness 装上自动挡。— END —苦猿 · 帮普通人把 AI 学进简历
返回列表