详情

首页手游攻略 Harness Engineering:让 Agent 在受控边界内运行

Harness Engineering:让 Agent 在受控边界内运行

佚名 2026-08-28 09:44:55

Harness Engineering:让 Agent 在受控边界内运行并不只看表面做法,关键还要理解相关条件、限制和后续影响。

写在前面:模型说“我能做”,不等于系统真该让它做

做 AI 桌面端工具时,给模型接工具并不难:注册一个 schema,写一个执行函数,模型就能发起调用。真正麻烦的是后半句——它能访问哪个目录?能不能写文件?要不要用户确认?取消后副作用怎么收尾?出了问题又该查谁?

Harness Engineering:让 Agent 在受控边界内运行

这些问题不会因为 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"]

它通常包括:

  1. 工具注册、发现、延迟暴露和禁用。
  2. MCP server 的连接、缓存、OAuth 与会话快照。
  3. 工作区、知识库、文件系统和频道的访问范围。
  4. 审批、自动批准与拒绝语义。
  5. 会话生命周期、流订阅、取消、恢复和持久化。
  6. 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 可以包含:

  1. Assistant 是否启用了 Web Search。
  2. 当前请求是否带有文件附件。
  3. 当前用户是否有知识库。
  4. Agent 的有效知识库范围。
  5. 模型是否具备工具调用能力。
  6. 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 被禁用时:

  1. 第一轮发现 ToolA
  2. 第二轮禁用 ToolB
  3. 第三轮禁用 ToolC

单次遍历是否足够取决于声明顺序,不动点算法则与工具注册顺序无关。

对于工具数量为 V、依赖边为 E 的小型注册表,该实现最坏约为 O(V × (V + E))。工具图通常很小,可读性与正确性优先于复杂的拓扑优化;若未来工具图大规模增长,再改为反向依赖图上的 BFS。

6. 关键算法三:工具延迟暴露与 token 预算

大型 MCP 生态会带来数百个工具 schema。将全部工具直接放入模型上下文会造成:

  1. Prompt token 膨胀。
  2. 模型在相似工具之间选择困难。
  3. 首 token 延迟和调用成本上升。

Cherry Studio 使用 deferred exposition:将一部分工具从初始工具集移除,改为提供 tool_searchtool_inspecttool_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。其价值在于:

  1. Prompt 中的工具指引与实际工具面一致。
  2. 知识库 scope 同时用于 Prompt、MCP bridge 和权限判断。
  3. Agent data directory 与工作区分开,避免长期记忆污染用户项目。
  4. 运行环境、插件、工具策略和审批回调在连接前就明确。

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)}}

关键不变量:

  1. Renderer 不直接写数据库。
  2. 只有 Main 能从 awaiting-approval 恢复流。
  3. 对 DB anchor 的写入必须确认目标 approval part 仍存在。
  4. Claude live session 和 MCP continuation 使用不同恢复机制,但共享同一个产品状态模型。

这避免了 overlay 先显示、数据库后落盘时的覆盖竞态,也保证多窗口看到同一份审批状态。

9. 关键算法六:工作区与 Agent 数据的双目录边界

Agent Session 同时需要:

  1. 用户工作区:代码、文档和任务文件所在目录。
  2. 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。因此必须明确:

  1. trace 默认仅在开发者模式启用。
  2. trace 文件视为敏感数据,不能默认上传或共享。
  3. “本地保存”不等于“不需要数据安全设计”。

11. 这些 Harness 坑,最好提前绕开

反模式:只在 Prompt 中声明权限

模型可能忽略、误解或被注入内容影响。必须在工具选择和执行时再次做强制检查。

反模式:把所有 MCP 工具直接放入模型上下文

这会增加 token、延迟和选择错误。应按使用频率与净 token 收益做延迟暴露,但审批工具必须例外。

反模式:工具禁用只做一次遍历

依赖链会留下半可用工具。应传播到不动点或使用反向依赖图。

反模式:Renderer 直接更新审批状态

多窗口、overlay 与持久化会产生竞态。审批状态必须由 Main 统一写入和恢复。

反模式:让慢 MCP 阻塞 Agent 启动

工具发现应使用 last-known-good cache 和后台刷新;产品应接受并显示最终一致性,而不是让聊天无法开始。

反模式:为了排障默认记录全部 Prompt 与工具 body

可观测性会变成敏感数据存储。应开发者模式门控、明确保存位置和导出策略。

12. 小结

Harness Engineering 的成熟标志不是“接入了很多工具”,而是系统能够清楚回答:

  1. 当前模型为什么能看到这个工具,而看不到另一个?
  2. 该工具真正执行前还会经过哪些权限、目录和审批检查?
  3. MCP 失效、缓存未预热或工具列表变化时,系统如何降级?
  4. 用户能否在不破坏执行状态的前提下批准、拒绝、取消或恢复任务?
  5. 是否能审计一次执行的模型、工具、审批、成本与敏感数据边界?

一句话总结:

继续阅读

  1. Tool Registry
  2. Tool Approval
  3. Agent Session Runtime
  4. Observability
  5. src/main/ai/runtime/claudeCode/settingsBuilder.ts
  6. src/main/ai/tools/adapters/claudeCode/toolConditions.ts
点击查看更多
推荐专题
热门阅读