Harness Engineering:让 Agent 在受控边界内运行
Harness Engineering:让 Agent 在受控边界内运行并不只看表面做法,关键还要理解相关条件、限制和后续影响。
写在前面:模型说“我能做”,不等于系统真该让它做
做 AI 桌面端工具时,给模型接工具并不难:注册一个 schema,写一个执行函数,模型就能发起调用。真正麻烦的是后半句——它能访问哪个目录?能不能写文件?要不要用户确认?取消后副作用怎么收尾?出了问题又该查谁?

这些问题不会因为 Prompt 写了“请谨慎操作”就自动消失。团队在研究 Cherry Studio 时,最值得借鉴的一点正是:模型负责提出动作,运行时负责决定动作是否能在正确边界内发生。
这层承上启下的控制系统,就是 Harness。
1. Harness 是什么
Prompt、Context 和 Loop 解决了模型如何理解任务、获得事实、推进多步骤执行的问题。Harness 解决的是另一个问题:
Harness 不是单个类,而是运行时的控制面:
flowchart LRusers["用户 / 外部渠道"] --> sessionGuard["会话与工作区校验"]sessionGuard --> toolPolicy["工具暴露与权限策略"]toolPolicy --> agentDriver["模型 / Agent Driver"]agentDriver --> execution["审批、工具执行、流式事件"]execution --> runtimeState["持久化、观测、恢复与 UI"]
它通常包括:
- 工具注册、发现、延迟暴露和禁用。
- MCP server 的连接、缓存、OAuth 与会话快照。
- 工作区、知识库、文件系统和频道的访问范围。
- 审批、自动批准与拒绝语义。
- 会话生命周期、流订阅、取消、恢复和持久化。
- trace、usage、错误和敏感数据的审计边界。
Harness 是将 Agent 从 Demo 变成产品的关键层。没有它,模型输出的工具调用只是未经约束的建议。
2. 先分清:模型看得见,不代表系统会放行
工具有至少四个不同状态:
registered工具在系统中存在visible 工具描述与 schema 进入模型上下文callable模型本轮可以发起调用executable运行时检查、审批通过后真正执行
它们不能合并为一个布尔值。否则“工具已经注册”很容易被误解为“这次调用一定能执行”。
例如一个删除文件的工具:
| 状态 | 结果 |
|---|---|
| registered | 工具实现已随应用安装。 |
| visible | 当前 Agent 能知道该工具存在。 |
| callable | 工具没有被用户禁用,且当前工作区满足前置条件。 |
| executable | 当前调用获得用户批准,输入也通过路径和权限校验。 |
Prompt 可以解释工具使用策略,但只能影响模型是否尝试调用;Harness 决定工具是否真正暴露和执行。
3. 回到 Cherry Studio:两条运行时,两套工具适配
Cherry Studio 有两条不同的工具适配路径:
flowchart LRsubgraph chatPath["普通 Chat"]chatRegistry["AI SDK ToolRegistry"] --> chatTools["内置工具 / MCP 工具 / meta tools"]chatTools --> chatAgent["AI SDK Agent"]endsubgraph sessionPath["Agent Session(Claude Code)"]sessionDescriptors["Claude Code tool descriptors + MCP servers"] --> sessionPolicy["disallowedTools + canUseTool"]sessionPolicy --> sessionDriver["Claude Code Runtime Driver"]end
两条路径共享产品能力,但不共享同一个工具注册实现。原因是 AI SDK 和 Claude Agent SDK 的工具协议、会话模型与权限回调不同。
这要求产品团队把领域能力与SDK 适配分开:
领域能力:KnowledgeService、FileManager、WebSearchService工具契约:schema、描述、权限级别、输出映射SDK 适配:AI SDK Tool / Claude MCP descriptor
不要把业务逻辑写进某个特定模型 SDK 的 tool callback;否则新增 Driver 时会复制权限和审计逻辑。
4. 关键算法一:每一轮只给模型必要的工具
普通 Chat 使用 ToolRegistry。每个注册项包含名称、namespace、描述、defer 策略、工具实现与 applies(scope) 谓词。
typeToolEntry = {name: stringnamespace: stringdescription: stringdefer: "never" | "always" | "auto"tool: Toolapplies?: (scope) =>boolean}
工具选择应按请求动态计算,而不是在应用启动时固定:
functionresolveActiveTools(registry, requestScope) {active = []for (entry of registry.entries()) {try {if (entry.applies && !entry.applies(requestScope)) {continue}active.push(entry)} catch (error) {logWarning("Tool applicability failed", entry.name, error)// fail closed:不能确认适用时,不向模型暴露}}return active}
requestScope 可以包含:
- Assistant 是否启用了 Web Search。
- 当前请求是否带有文件附件。
- 当前用户是否有知识库。
- Agent 的有效知识库范围。
- 模型是否具备工具调用能力。
- Provider 是否支持某类原生能力。
这让工具面成为当前请求的函数:
ToolSurface = f(assistant, model, provider, session, permissions, request)
而不是简单的全局数组。
5. 关键算法二:依赖传播到不动点
工具之间可能存在依赖。例如 ExitWorktree 依赖 EnterWorktree;若前者可见而后者被禁用,模型会看到一个不可完成的动作。
Claude Code 路径的 resolveDisallowedTools 使用不动点算法传播禁用状态:
functionresolveDisallowedTools(toolDefinitions, userDisabled, runtimeContext) {blocked = newSet()// 第一轮:直接禁用for (tool of toolDefinitions) {if (tool.exposure === "disabled") {blocked.add(tool.name)continue}if (tool.exposure === "user" && userDisabled.has(tool.name)) {blocked.add(tool.name)continue}predicate = tool.enablePredicateif (predicate && !predicate(runtimeContext)) {blocked.add(tool.name)}}// 后续轮:禁用依赖于已禁用工具的工具changed = truewhile (changed) {changed = falsefor (tool of toolDefinitions) {if (blocked.has(tool.name)) continueif (tool.dependsOn.some(dep => blocked.has(dep))) {blocked.add(tool.name)changed = true}}}return [...blocked]}
为什么需要循环
设依赖链为:
ToolC → ToolB → ToolA
当 ToolA 被禁用时:
- 第一轮发现
ToolA。 - 第二轮禁用
ToolB。 - 第三轮禁用
ToolC。
单次遍历是否足够取决于声明顺序,不动点算法则与工具注册顺序无关。
对于工具数量为 V、依赖边为 E 的小型注册表,该实现最坏约为 O(V × (V + E))。工具图通常很小,可读性与正确性优先于复杂的拓扑优化;若未来工具图大规模增长,再改为反向依赖图上的 BFS。
6. 关键算法三:工具延迟暴露与 token 预算
大型 MCP 生态会带来数百个工具 schema。将全部工具直接放入模型上下文会造成:
- Prompt token 膨胀。
- 模型在相似工具之间选择困难。
- 首 token 延迟和调用成本上升。
Cherry Studio 使用 deferred exposition:将一部分工具从初始工具集移除,改为提供 tool_search、tool_inspect 和 tool_invoke。
初始上下文:少量常用工具+ tool_search / tool_inspect / tool_invoke+ 可用 namespace 摘要模型需要罕见工具时:tool_search → tool_inspect → tool_invoke
是否 defer 不能只比较工具数量。meta tools 自身有固定 Prompt 成本,因此必须判断净收益:
functionshouldDefer(entries, contextWindow) {autoCandidates = entries.filter(entry => entry.defer === "auto")if (autoCandidates.length < MIN_AUTO_DEFER_COUNT) {return []}estimatedSavedTokens = estimateSchemasTokens(autoCandidates)metaToolsCost = META_TOOLS_OVERHEAD_TOKENSif (estimatedSavedTokens <= metaToolsCost) {return []}returnchooseDeferredEntries(autoCandidates, contextWindow)}
这是一个典型的成本模型:
netSaving = inlineToolSchemaTokens - metaToolStaticTokens - expectedDiscoveryTokens
若 netSaving <= 0,延迟暴露会增加而不是减少成本。
6.1 审批工具永不 defer
审批工具必须保持 inline:
functionclassifyDeferPolicy(tool) {if (tool.needsApproval) {return"never"}return tool.defer}
如果审批工具被 defer,模型可以通过 tool_invoke 间接调用;原 SDK 的审批 gate 可能无法触发。Cherry Studio 同时在 meta tool 的执行路径再次拒绝 approval-gated 工具,形成双重防线。
结论是:
7. 关键算法四:Agent Session 的运行时装配
Harness 的重要职责是在连接 Agent Driver 之前构建一份一致的、可冻结的运行时配置。
Agent Session 的 settings 构建可简化为:
asyncfunctionbuildSessionSettings(session, provider, options) {assertSessionHasAgentAndWorkspace(session)awaitprepareWorkspaceDirectory(session.workspace)// 并行执行互不依赖的初始化[agentDataPath, env, workspacePlugins] = awaitPromise.all([ensureAgentDataDirectory(session.agentId),buildEnvironment(provider, session.agent),discoverWorkspacePlugins(session.workspace.path)])warmResult = awaitwarmMcpToolCaches(session.agent)permissions = awaitbuildToolPermissions(session,session.agent,agentDataPath)knowledgeScope = resolveKnowledgeBaseScope(session.agent.knowledgeBaseIds,options.selectedKnowledgeBaseIds)prompt = awaitbuildSystemPrompt({session,agent: session.agent,cwd: session.workspace.path,agentDataPath,knowledgeScope,disallowedTools: permissions.disallowedTools})mcpServers = buildMcpServers({session,agent: session.agent,knowledgeScope})return {cwd: session.workspace.path,additionalDirectories: [agentDataPath],env,plugins: workspacePlugins,systemPrompt: prompt,mcpServers,canUseTool: permissions.canUseTool,disallowedTools: permissions.disallowedTools}}
这份 settings 是 Agent Session 的 capability snapshot。其价值在于:
- Prompt 中的工具指引与实际工具面一致。
- 知识库 scope 同时用于 Prompt、MCP bridge 和权限判断。
- Agent data directory 与工作区分开,避免长期记忆污染用户项目。
- 运行环境、插件、工具策略和审批回调在连接前就明确。
7.1 慢 MCP 的 bounded warm 与最终一致性
MCP 服务可能慢或不可用。若每次会话启动都同步 listTools,一个故障服务就会阻塞聊天。
因此热路径读取 last-known-good cache;首次冷缓存时触发后台刷新:
asyncfunctionlistToolsWithoutBlocking(serverId) {cached = cache.get(`mcp.tools.${serverId}`)if (cached is undefined) {voidrefreshToolsInBackground(serverId)}return cached ?? []}
这带来最终一致性:本次 session 在缓存尚未预热时可能看不到某些工具,后续 session 才会看到。对于 Claude Agent SDK,工具列表在会话建立时快照化,不能在同一会话中任意扩容。
settingsBuilder 对 bounded warm 超时的场景会在后台预热完成后,刷新工具元数据与 policy snapshot,避免“模型可见工具”和“审批 UI 元数据”长期不一致。
8. 关键算法五:审批状态的单写者模型
工具审批涉及 Renderer、Main、数据库和可能仍在运行的 Agent Driver。若任何一方都能直接改审批状态,竞态会迅速出现。
Cherry Studio 采用 Main 单写者:
Renderer:展示 approval card,提交用户决定Main:验证、写入权威状态、恢复对应运行时
概念状态机:
stateDiagram-v2[*] --> requestedrequested --> approvedapproved --> executingexecuting --> resolvedrequested --> denieddenied --> resolvedrequested --> abandonedabandoned --> resolved
伪代码:
asyncfunctionrespondToApproval(request) {live = approvalRegistry.get(request.approvalId)if (live.belongsToClaudeAgentSession) {// 解除 canUseTool 上等待的 promise;无需读写普通 Chat 消息行live.resolve(request.decision)return}anchor = messageService.getById(request.anchorId)part = findApprovalPart(anchor.parts, request.approvalId)if (!part) {// Renderer 可能先于持久化看到 overlay;不能盲写覆盖数据库return}updatedParts = applyApprovalDecision(anchor.parts, request.decision)messageService.update(anchor.id, { parts: updatedParts })if (allApprovalsResolved(updatedParts)) {dispatchContinueConversation(anchor)}}
关键不变量:
- Renderer 不直接写数据库。
- 只有 Main 能从
awaiting-approval恢复流。 - 对 DB anchor 的写入必须确认目标 approval part 仍存在。
- Claude live session 和 MCP continuation 使用不同恢复机制,但共享同一个产品状态模型。
这避免了 overlay 先显示、数据库后落盘时的覆盖竞态,也保证多窗口看到同一份审批状态。
9. 关键算法六:工作区与 Agent 数据的双目录边界
Agent Session 同时需要:
- 用户工作区:代码、文档和任务文件所在目录。
- Agent 数据目录:SOUL、USER、FACT、JOURNAL 等跨会话记忆。
它们不能混为同一目录。
cwd = session.workspace.pathadditionalDirectories = [agentDataPath]
概念校验:
functionvalidateFileOperation(targetPath, workspacePath, agentDataPath) {if (isInside(targetPath, workspacePath)) {returnallow()}if (isInside(targetPath, agentDataPath)) {returnallow()}return requireUserApproval("Path is outside the current workspace")}
这一边界使 Agent 能维护自己的长期身份和记忆,同时不能因为拥有 Agent 数据目录就任意读取用户磁盘。
目录校验不应只使用字符串前缀比较,应使用规范化、真实路径解析和 symlink 防护。否则:
/workspace-safe/../secret/workspace-safe-link → /secret
可能绕过简单的 startsWith("/workspace-safe") 检查。
10. 关键算法七:Trace、Usage 与敏感数据边界
Harness 还负责回答“这次 Agent 到底做了什么”。
Cherry Studio 为 AI SDK 调用建立 span tree:
chat.turn├─ ai.streamText├─ ai.streamText.step├─ ai.toolCall└─ usage / model / topic attributes
概念实现:
asyncfunctionrunObservedTurn(request) {root = trace.startSpan("chat.turn", {topicId: request.topicId,modelName: request.modelName})try {stream = await aiService.streamText(request, root.context)result = awaitpipeAndPersist(stream)root.setStatus("ok")return result} catch (error) {root.recordException(error)root.setStatus("error")throw error} finally {root.end()traceStorage.flush(request.topicId)}}
Trace 并非默认无害。开发模式下 Claude Code 的 verbose telemetry 可包含用户 Prompt、工具内容甚至原始 API body,并被写入本地 JSONL。因此必须明确:
- trace 默认仅在开发者模式启用。
- trace 文件视为敏感数据,不能默认上传或共享。
- “本地保存”不等于“不需要数据安全设计”。
11. 这些 Harness 坑,最好提前绕开
反模式:只在 Prompt 中声明权限
模型可能忽略、误解或被注入内容影响。必须在工具选择和执行时再次做强制检查。
反模式:把所有 MCP 工具直接放入模型上下文
这会增加 token、延迟和选择错误。应按使用频率与净 token 收益做延迟暴露,但审批工具必须例外。
反模式:工具禁用只做一次遍历
依赖链会留下半可用工具。应传播到不动点或使用反向依赖图。
反模式:Renderer 直接更新审批状态
多窗口、overlay 与持久化会产生竞态。审批状态必须由 Main 统一写入和恢复。
反模式:让慢 MCP 阻塞 Agent 启动
工具发现应使用 last-known-good cache 和后台刷新;产品应接受并显示最终一致性,而不是让聊天无法开始。
反模式:为了排障默认记录全部 Prompt 与工具 body
可观测性会变成敏感数据存储。应开发者模式门控、明确保存位置和导出策略。
12. 小结
Harness Engineering 的成熟标志不是“接入了很多工具”,而是系统能够清楚回答:
- 当前模型为什么能看到这个工具,而看不到另一个?
- 该工具真正执行前还会经过哪些权限、目录和审批检查?
- MCP 失效、缓存未预热或工具列表变化时,系统如何降级?
- 用户能否在不破坏执行状态的前提下批准、拒绝、取消或恢复任务?
- 是否能审计一次执行的模型、工具、审批、成本与敏感数据边界?
一句话总结:
继续阅读
- Tool Registry
- Tool Approval
- Agent Session Runtime
- Observability
src/main/ai/runtime/claudeCode/settingsBuilder.tssrc/main/ai/tools/adapters/claudeCode/toolConditions.ts
-
08.28
迷你世界怎样可以召唤远古巨人
-
08.28
梦境护卫队新手氪金建议
-
08.28
《穿越火线:枪战王者》新势力M7说明
-
08.28
梦境护卫队装备系统说明
-
08.28
《鸣潮》凌阳声骸搭配攻略
-
08.28
龙石战争中西蒙如何配对
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏