资讯动态

从Claude Code抄作业:AI代理执行框架的上下文、权限与恢复设计

发布时间:2026/10/20 12:55:23 来源:尧图企业网站定制
最近两周我把大部分业余时间都花在了Harness的设计与实现上。Harness是我给一个AI代理执行框架起的内部代号核心目标就是让大模型稳定地完成多步任务接收自然语言需求、拆解执行步骤、调用外部工具、最后汇总结果。这项目其实不大总共也就几千行代码但真正难的不是写代码而是想清楚每个环节该怎么设计。中间我几次推翻重来最后让我稳定下来思路的反而是我自己日常用Claude Code写别的项目时积累下来的使用体验。这篇文章就记录一下我从Claude Code的产品设计里“抄了哪些作业”以及这些启发是怎么一步步落到Harness代码里的。1. 先说明白Harness是什么以及我最初的设计误区1.1 项目代号的含义先给项目定个位。在AI Agent这个圈子里harness本身是个常见词意思是把模型、工具、外部状态全部绑在一起的“控制环”。模型不自带工具调用能力也不自带持久记忆需要外面有一层东西替它接好管线、管好状态、处理好失败情况。这层东西正是我这段时间在做的Harness。它处理的任务场景大致长这样用户提一个需求比如“把这个目录下所有测试用例跑一遍失败的分三类汇总”Harness负责判断该调用哪个测试命令、该读哪些日志、该按什么格式汇总最终把结果交还给用户。听起来简单但一旦任务超过两三个步骤模型就开始不稳定中途忘记需求、重复调用同一个工具、在无关文件上浪费时间。我一开始以为这只是模型能力问题后来才发现更大的问题出在我这个外壳设计上。1.2 我一开始犯下的三个设计错误第一个错误把流程控制设计成固定瀑布。我给每个任务预先画好步骤图规定模型第一步做什么、第二步做什么模型几乎没有自主判断的空间。真实任务根本没有那么标准的边界用户随时补一句“先跳过无法编译的模块”整个流程就卡死了。第二个错误把上下文当成大水缸什么都在里倒。早期Harness把用户需求、历史对话、工具返回一股脑塞进同一个消息数组很快token就爆了。更麻烦的是一旦超长大模型真正能“看到”的内容其实只集中在末尾前面用户强调过的约束条件全部被截断于是模型开始反复追问用户已经给过的信息。第三个错误权限做成“全有或全无”。我图省事直接给模型一个能读文件、能执行Shell命令、能改配置的通用工具包完全没设计确认机制。结果有一次模型在清理临时文件时顺手把一个目录下的中间产物全删了吓得我把所有权限全部收回收回之后模型又干不了活连读日志都得先复制路径再手动打开。这三个问题互相纠缠让我意识到Harness缺的不是模型能力而是“管理层能力”。也正是在这个时候我开始认真观察Claude Code在真实开发场景里是怎么处理上下文、怎么授权、怎么展示过程的后面的几个章节就是这些观察落地到代码里的全过程。2. 第一课上下文不是堆token而是分层遗忘2.1 我看到Claude Code对长会话的处理日常开发里我经常把整个项目结构、最近的改动、各种报错信息一起交给Claude Code处理。这个工具的界面上会显示上下文窗口的使用量而且跑着跑着早期的内容会被折叠起来。最开始我以为是简单粗暴的截断但后来测试发现一个非常关键的现象即使过期的消息被折叠我在后续对话里问它之前提过的某个函数名、某个路径、某个取舍理由它依然能答上来。这说明它保存的不只是“原始消息”而是经过摘要和关键事实提取之后的“高层记忆”。细节被压缩进摘要但事实没有丢。这和我过去那种“只要是历史就全留着”的上下文设计思路完全反过来它追求的不是全量缓存而是在有限窗口里尽量保留“有价值的信息”。2.2 Harness里的ContextWindow摘要层、事实层、消息环受这个现象启发我把Harness的状态结构整个重写了。现在不再是一个message数组而是三层结构summary对当前整个会话的压缩摘要描述已经完成了什么、当前处于什么阶段facts早期内容里抽出来的关键事实比如文件路径、接口字段、依赖约束、用户明确表达过的偏好recent_messages最近几条完整消息保留模型的近期工作记忆。一个简化版本的ContextWindow类大概长这样class ContextWindow: def __init__(self, max_tokens120_000): self.summary self.facts: list[str] [] self.messages: list[Message] [] self.max_tokens max_tokens def add(self, message: Message): self.messages.append(message) self._maybe_compress() def _maybe_compress(self): if self.estimate_tokens() self.max_tokens * 0.7: return old self.messages[:-6] self.summary summarize_messages(old, base_summaryself.summary) self.facts.extend(extract_key_facts(old)) self.messages self.messages[-6:]这个类的关键在于两个时机。第一压缩触发点设在窗口使用量的70%而不是等到满了再截断第二压缩时把旧消息分成两类过程型信息进summary事实型信息进facts。压缩之后recent_messages只保留一个小尾巴token占用通常能直接降一半以上。相比原来“堆到极限再丢弃”这种分层方式让模型在长时间任务里始终保有最近细节也保有最重要约束。2.3 压缩过头引发的翻车教训第一次实现这个模块时我把摘要“做得太狠”早期内容一律只留下一句话摘要比如“用户要求实现登录与注册模块”。结果任务跑到一半模型在做注册模块的接口对接时需要知道某个字段的校验规则而这些规则早就被揉进了粗糙的摘要里。模型干脆自己脑补了一套规则生成的代码完全不符合预期。后来我在压缩逻辑里加了一条硬规则凡是事实型信息例如接口字段、文件路径、依赖版本、用户明确否定的方案必须写进facts列表甚至不允许被压缩只有过程型信息例如中间报错、临时输出、闲聊式的补充说明才允许进summary。这条规则看起来简单实际效果非常明显再跑长时间任务时模型很少再出现“失忆”的情况。目前我的默认配置是保留最近6条完整消息facts最多保留30条summary会随着压缩次数不断重建再往上就靠后面第5节要讲的检查点来做跨会话恢复。3. 第二课工具权限要是“按路径信任”而不是“全有或全无”3.1 Claude Code给我的权限启发Claude Code在调用工具时会明确区分“这个操作需要你确认”还是“这个操作我可以直接帮你跑”。读取一个文件通常直接执行执行Shell命令、写文件、调整配置它往往会先把命令和参数展示出来等你确认再继续。更妙的是同一个会话里确认过一次之后同类型的操作不会反复打断你。这背后是一种路径级别的信任机制权限的控制粒度不是“要么给、要么不给”而是“按操作类型、按具体路径、按会话窗口”动态放开。我原先那种“一个工具包全放开”的设计相当于把所有门都敞开而Claude Code的做法更像给每个房间配了不同的门禁只把常用的一条走廊设为常开。3.2 Harness里的PermissionPolicy模型我在Harness里做了一个权限策略模块。每个工具注册时必须声明自己的策略没有默认放行这个选项tools { read_file: PermissionPolicy(policyauto_allow, modes[readonly]), list_dir: PermissionPolicy(policyauto_allow, modes[readonly]), run_shell: PermissionPolicy( policyneed_approval, allow_paths[/workspace/build, /workspace/scripts] ), write_file: PermissionPolicy( policyneed_approval, allow_paths[/workspace/src] ), delete_path: PermissionPolicy(policydeny), }auto_allow表示只读、无副作用的操作可以直接执行need_approval表示有副作用的操作必须进入pendingApproval状态等用户点击确认deny表示默认禁止高风险操作宁可让用户自己复制命令出去执行。need_approval里的allow_paths字段含义是“在这个路径范围内如果本次会话已经确认过一次后续类似操作自动放行”。我特意把这种信任做成会话级而不是全局级原因很简单全局放行一次意味着以后所有同类操作都放行风险太高会话级放行则意味着新开会话自动回到“默认谨慎”状态。这样做的副作用是每轮会话开头模型会多问几次但整体安全性明显提升。3.3 权限确认频率太高怎么办第一次把权限模块接上之后问题变成了“确认次数太多”模型每跑一条命令UI上就弹一次申请连续几条命令下来体验比手动操作还累。我调整的办法是给工具按“破坏面”打分ls、grep、find这类纯查询命令直接放进auto_allowmv、rm、curl、npm install这类有副作用的命令进need_approval任何涉及生产数据目录、部署脚本、发布流程的命令全部deny宁可让用户手动执行。这套策略上线后权限确认次数降了大约七成同时没有再次出现模型私自改动关键文件的情况。另外一个容易被忽略的细节是用户的“点击确认”动作本身也要写审计日志包括确认人、确认时间、完整命令行、当时的上下文意图。这样事后如果出现误操作至少能回答“是谁、为了什么、怎么放行的”这几个问题。4. 第三课让用户看得见“正在发生什么”——渐进式披露的可观测性4.1 为什么传统日志模型对AI代理不适用调试传统后端程序靠的是日志级别加调用栈因为执行路径是可预测的。但AI代理不一样同一句提示词两次运行可能走完全不同的工具调用序列。假如只是在关键位置打几个普通日志你根本判断不了它“为什么选择这一步”以及“上一步到底用了多久”。我在Harness早期就遇到过这种情况一个任务从下午跑到晚上中途反复调用同一个工具七八次从普通日志里完全看不出它是在循环、卡住还是真的在按计划执行。这时候需要的不是更好看的日志而是一套行为轨迹记录。4.2 AgentEvent把所有行为都变成结构化TraceClaude Code的输出方式是“一步一步展示”每一步都有缩进层级无关紧要的中间文件内容会被折叠结论性信息则摊开给用户看。这种展示方式的本质是渐进式披露。我把这个思路搬进Harness设计了一个统一的事件模型dataclass class AgentEvent: ts: float run_id: str parent_id: str | None type: str # step_started / tool_called / tool_result / llm_request summary: str detail: dict每产生一个step_started就生成一个新节点父节点是它从哪一步派生出来的每调用一次工具记录tool_called工具返回后记录tool_result。因为事件自带parent_id任务运行结束后UI可以直接渲染成一棵调用树。每个节点上挂summary和detailsummary是一句话detail是完整内容默认只展示summary用户点开才看detail。4.3 三层展示和一个定位问题的真实场景实际界面我做成三层第一层是一行事件摘要例如“运行单元测试 / 用时3.2秒 / 返回128个case通过”第二层点开是子事件列表第三层才把工具参数、模型输出原文全部摊开。刚开始我觉得这样已经够用了后来发现还少一个维度时间。于是我在每个事件里额外记录了整棵子树里最慢的那个节点的耗时。有一次排查“任务莫名停顿40秒”的问题靠树形Trace定位到停顿根本不在工具执行而是一次模型请求某条系统提示词里塞了一个巨大的JSON配置模型光理解它就多花了40秒。这种问题用普通日志根本发现不了因为日志只会显示工具调用延时正常对模型内部的“思考时长”一无所知。结构化的行为轨迹还有一个巨大好处它能回答“这一步在哪一秒开始、哪一秒结束、调用方是谁”。后来同事反馈某个任务慢我直接把Trace导出成JSON塞给模型分析几分钟就能找出重复调用和无效规划的位置不用人肉盯着日志翻。5. 第四课会话恢复把一次性请求变成可持续运行的工程流程5.1 一次运行不该是一次性请求Claude Code支持会话恢复窗口关了、机器重启再打开还能接着聊。这个能力的背后是把对话状态持久化而不是每次当作全新请求启动。这个体验放到Harness里意义远不止“方便”一个多步任务可能跑很久中途会遇上网路抖动、工具超时、进程被杀。如果没有恢复能力失败一次就得从头重跑时间和token都扛不住。所以我直接把“可恢复”定义为Harness的硬性要求任何一次多步运行都必须能从最近的检查点继续而不是从零开始。5.2 检查点与恢复流程怎么落地实现上我用一个SQLite文件保存会话元数据和检查点核心表就两张CREATE TABLE sessions ( id TEXT PRIMARY KEY, created_at INTEGER, updated_at INTEGER, status TEXT ); CREATE TABLE checkpoints ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, seq INTEGER, state_json TEXT, tool_output_summary TEXT, token_usage INTEGER );session是运行实例checkpoint每一行是一个可恢复断点。state_json里存当前上下文、已完成步骤、待执行队列、facts列表等tool_output_summary只存摘要不存完整输出。恢复流程很直接启动时传入session_id读取seq最大的那个checkpoint把state_json还原成内存里的ContextWindow然后从待执行队列继续跑。这中间最关键的原则是检查点保存的是“下一步要做什么”而不是“让模型重新推理一遍”。早期我试过保存完整思维链恢复时丢给模型让它自己接上效果很差模型经常跑偏。改成保存待执行队列之后恢复行为几乎和正常流程一致因为模型不需要重新规划只需要继续执行。5.3 存什么、不存什么存储膨胀的坑我踩过的一个很实在的坑是把工具的stdout完整写进checkpoint。有个任务会拉取一个几十MB的中间产物我直接序列化进了state_json结果SQLite文件瞬间膨胀到几百MB恢复时反序列化的耗时比重跑还长。后来改成完整stdout只在Trace里保留固定窗口checkpoint只写大小、hash、摘要和读取位置。如果恢复后确实需要原始内容按hash去缓存目录查找缓存被清掉的话就标记当前这一步重新执行。这样调整之后检查点文件稳定控制在几百KB以内。这个能力上线后效果非常直观。CI里跑一个依赖外部接口的长期任务中间某一步网络超时以前是全部重跑现在脚本检测到失败后自动拉起同一个session从失败那一步继续用户端除了多等几十秒整体流程基本无感。6. 重构前后的实测数据以及两个躲不开的取舍6.1 我在本机跑的一组小规模对比数据这一节不是严谨的学术评测只是用我自己攒的几个真实任务在同一批API key上跑出来的前后对比主要给方向参考指标重构前重构后备注单任务平均token消耗63K42K上下文分层压缩让早期内容不再重复占用多步任务成功率64%89%facts层保留关键约束不再“失忆”平均单任务耗时37秒29秒权限确认减少了大量无效路径长任务中途失败后恢复成本全部重跑从断点继续节省约60%的调用量一次误操作回溯定位时间数小时几分钟结构化Trace加审计确认记录这些数字换一个任务集、换一个模型后端比例关系大概率会变。但里面最有普适性的两个结论是上下文压缩几乎总能节省20%到40%的token权限确认和结构化Trace加在一起能把“长任务失败”从一次灾难变成常规可恢复事件。6.2 这些设计也带来了新的成本第一个成本是确认骚扰。权限设计必须反复调优白名单否则用户会觉得模型干什么都要问一下还不如手动操作。我的经验是宁可在开头多问几句也不要让一个危险操作悄悄通过。第二个成本是日志膨胀。全量Trace意味着数据生成量远超普通应用日志所以我在Trace上加了两条限制完整detail默认保留7天超过一周只保留summary和事件类型单个事件的detail字段超过一定大小就截断并落盘成独立文件Trace里存文件路径。如果不加这层限制一个小工具跑一天就能吃掉几个GB磁盘。最开始我全量保留了一个星期发现磁盘占用完全扛不住才被迫加上保留策略。6.3 我对“让AI观察AI”的新想法做完这些重构之后我现在更想做的事是让Harness定期用一次额外的模型调用去“观察”自己的行为轨迹然后生成一段简短的自我复盘哪些步骤重复了、哪个工具被过度调用、哪个决策偏离了原始需求。Claude Code里“当前状态摘要”的呈现方式给了我一个方向这类观察应该作为旁路运行不掺进主流程的指令否则模型会试图修改自己的执行路径很容易跑飞。目前这个想法还在实验节点验证等数据攒一攒可能会单独再写一篇。7. 收尾最值得抄的不是某个功能而是它对“人机协作边界”的理解我反复用Claude Code的过程中感触最深的不是某个具体功能而是它对“模型能做什么、不能做什么”的边界设定。它几乎不假设模型是可靠的每一步都让你看到证据给你确认权限允许你随时打断和恢复。过去我做工具设计习惯性把模型当成主力Harness只需要把力气活接好就完事现在反过来Harness更像一套安全带和仪表盘——让模型放手去试但每一步都有护栏随时能停下来、能回来、能解释。如果想把类似的启发搬进自己的项目我的建议是顺序可以这样安排先做上下文分层和检查点恢复这两个解决“任务能不能跑完”的问题再做权限策略和结构化Trace这两个解决“用户敢不敢用”的问题最后才考虑自动复盘和策略优化这些锦上添花的功能。从实际效果看前面四步已经足够让一个内部工具从“演示级”变成“能让同事连续用一周”的程度。希望这篇记录对正在折腾类似AI Agent框架的人有点实际帮助。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑