
坐在云栖大会的专场里Kymo 分享到后半段时我原本只是在记一些 Agent 编排的要点结果屏幕切到一张调用链路图图上明确标出了“Harness 引擎”和“MCP 审计”两个模块现场提问环节立刻有人举手问这两个东西是不是配套的。我当时也有同样的疑问。回来之后我花了大半个周末把这两个方向串起来研究了一遍顺手在内部系统里复现了一个最小可跑的版本。这篇文章就是那段时间的完整记录。如果你正在做 AI Agent 应用或者你们团队已经接了 MCP 工具但发现“调用黑盒化、权限收不住、出了问题查不到”那这篇文章会比较有用。我会从 Kymo 分享里听到的设计思路出发把 Harness 引擎的定位、MCP 审计方案的核心设计、以及我落地时踩过的坑全部展开讲不绕弯子。1. 云栖现场的启发Kymo 到底在解决什么问题1.1 MCP 生态越繁荣Agent 失控越容易先聊一个现象。MCPModel Context Protocol模型上下文协议这一年多的扩散速度比我预想中快得多。以前接一个工具能力要写插件、写适配层、对齐参数现在只要对方实现了 MCP Server客户端这边几乎是拿到即用。我随手列几个身边团队正在接的设计稿对接用 Figma MCP、蓝湖 MCP浏览器自动化有浏览器 MCP低代码平台有 Dify 的 MCP 接入数据库场景里连 IDE 插件都在做 Oracle 的 MCP 连接。往下走一点调试器、逆向工具链里也出现了 MCP 桥接插件甚至游戏引擎 Unreal 5.8 的工具链都在往 MCP 靠。这带来的直接问题是模型能碰的东西越来越多但你真正“管得住”的东西越来越少。以前你给模型开放一个函数调用至少要在代码里写清楚这个函数的入参出参、权限范围出了问题能顺着代码往上查。MCP 把这件事简化成了一个“发现工具、声明参数、发起调用”的标准流程服务端和客户端之间只通过 JSON-RPC 通信。好处是生态打通了坏处是所有工具调用长一个样你在调用层想做权限控制、参数校验、链路追踪反而没了抓手。Kymo 分享里那张链路图就是在回答一个问题当 Agent 可以调用几十个甚至上百个 MCP 工具时怎么让每一次调用都可控、可审、可追溯。他把答案拆成了两层一层负责执行管控叫 Harness 引擎一层负责事后留痕与分析就是 MCP 审计方案。这两个东西不是二选一而是一条链路的两端。1.2 Harness 引擎和 MCP 审计方案的分工Harness 这个英文词本义是“马具、挽具”引申过来就是“给 Agent 装上缰绳”。Kymo 团队说的 Harness 引擎我理解下来并不是又一个 Agent 编排框架而是一个插在“模型发起工具调用”和“实际执行工具调用”之间的控制层。它不负责 Agent 怎么思考、怎么规划只负责工具这扇门怎么开、开给谁、开了之后留什么证据。MCP 审计方案则完全不同。审计关注的是链路和证据也就是每一次调用到底由哪一轮对话触发、发给了哪个 MCP Server、传了什么参数、返回了什么结果、耗时多少、最终成功还是失败。这些数据不是为了实时控制而是为了事后回答“刚才是谁让模型去执行了那个操作”以及“如果出问题了问题出在哪个环节”。把这两层放在一起看逻辑就很顺Harness 引擎管“能做什么”MCP 审计方案管“做了什么”。一个偏实时一个偏离线一个负责改行为一个负责留证据。我在复现的时候是把他们当成一整套方案来设计的下面逐步拆解。提示Kymo 分享里涉及的实现级细节其实不算多很多设计思路我是在现场边听边记录的。这篇文章里的工程级补充一半来自分享内容一半来自我回来后在内部 Agent 平台上的复现实践。2. Harness 引擎的技术拆解它到底拦截了什么2.1 Harness 不是 Agent 框架而是一个“工具网关”很多团队一听“引擎”两个字下意识会问能不能替换 LangChain、替换我们自己写的 Agent 编排我在研究完之后得出的结论是Harness 引擎的定位不是替换编排层而是替编排层看门。打个比方Agent 编排层是快递分拨中心负责根据面单决定包裹往哪走Harness 引擎则是分拨中心出口处的安检通道。它不参与分拣逻辑但每一件包裹离开发货区之前必须从它这里过一遍。这个位置很关键因为它天然卡住了所有工具调用的必经路径。只要 Agent 最终要发起 MCP 调用请求就一定经过 Harness不需要在每个 MCP Server 里单独植入逻辑。把这个设计落地到工程上一个典型的 Harness 引擎通常会包含这么几个能力MCP 客户端管理维护一组 MCP Server 的连接负责工具发现和调用转发统一工具目录把多个 Server 暴露的 tools 汇总成一个可检索、可筛选的目录策略引擎在调用前评估“这次调用是否被允许”策略可以是权限类、频率类、参数类审计事件生产者把调用前、调用后、异常时的关键信息落成结构化事件。我在内部系统里复现时并没有把 Harness 设计成一个独立服务而是做成了一个库加一个 sidecar 网关的组合形态。Agent 服务通过 SDK 接入 HarnessSDK 内部维护 MCP Client同时审计事件异步上报给轻量网关由网关负责落库。这样做的好处是Agent 侧不用关心 HTTP 服务细节而审计数据不占用 Agent 进程的资源。2.2 MCP 客户端、工具发现与统一工具目录要理解 Harness 为什么能统一拦截得先搞清楚 MCP 的通信模型。MCP 协议里有三个核心角色MCP Server 负责暴露工具MCP Client 负责发起调用人类或上层应用作为调用方。工具本身是用 JSON-Schema 描述参数的按约定声明 name、description、inputSchema 等字段。传输层常见有三种标准输入输出stdio、HTTP 长连接、SSE 流式传输。stdio 适合本地子进程场景比如跑一个本地文件工具HTTP 和 SSE 适合跨服务场景比如接一个远程设计稿服务。Harness 引擎在这里扮演的角色是一个“多路 MCP Client”。它会同时连接若干个 MCP Server启动时或周期性执行一次tools/list调用把所有 Server 暴露的工具全部拉回来合并进统一工具目录。目录里不只有工具名和参数模型我还会额外维护三个元信息字段来源 Server、调用协议、安全等级。举个例子这是我简化后的工具目录配置结构{ version: 2025.06, servers: [ { name: internal-filesystem, transport: stdio, command: [node, fs-server.js], toolsPrefix: fs, securityLevel: high }, { name: vector-db, transport: http, baseUrl: http://localhost:8300/mcp, toolsPrefix: vector, securityLevel: medium }, { name: external-search, transport: http, baseUrl: http://search-gateway:8320/mcp, toolsPrefix: web, securityLevel: low } ] }为什么要在工具名前加前缀因为多个 MCP Server 很可能暴露同名工具比如search这个工具名在搜索服务和内部知识库里都可能存在。合并目录时不加前缀Agent 看到的就是一个冲突且不含来源信息的工具集合调用时容易发错目标。加上来源前缀之后LLM 看到的工具名是vector_search、web_search工具目录自带命名空间调用路径天然清晰。这个细节我强烈建议大家在设计工具目录时提前考虑否则线上会频繁出现“看起来调用了 search结果是从错误的数据源返回的结果”。2.3 调用链路上的钩子与策略点工具目录建好之后真正的核心是调用拦截。Harness 引擎在一个 MCP 调用请求被转发出去之前会先经过一系列钩子。我把这些钩子按执行顺序分成三类前置检查、调用转发、后置处理。前置检查包括会话鉴权、工具级白名单、参数级校验、频控判断。这一步做的是“让不让过”的决定如果决定是“不让过”Harness 会直接向 Agent 返回一个拒绝原因并生成一条拒绝审计事件。这里的要点是拒绝要显式返回给 Agent不能静默丢弃。否则模型会一直认为工具调用已成功继而基于幻觉继续往下规划。调用转发则相对直接。Harness 把原始请求通过 MCP Client 转发给对应 Server等 Server 返回结果或异常。这一步真正容易出问题的地方在参数序列化。MCP 的输入参数本质上是一段自由 JSONAgent 经常会生成多层嵌套结构如果只是简单透传有些 Server 会解析失败。我在转发前会做一步“参数清洗”把明显不符合目标工具 JSON-Schema 的字段剥掉并且把字符串形式的数组恢复成真正的数组这能显著降低调用失败率。后置处理是三个环节里最重要但最容易被忽略的。调用返回后Harness 需要把结果做摘要化处理再写审计事件。模型拿到的完整输出可能很大但审计并不需要存全部内容存摘要和关键字段就足够。同时敏感字段必须在这里做脱敏而不是等落库了再处理。这个顺序问题我放到后面单独讲。下面是我在实际代码里使用的一段简化示例用于展示一次 MCP 工具调用的拦截骨架async function handleToolCall(toolCall: ToolCall, session: Session) { const policy await policyEngine.evaluate(session, toolCall); if (!policy.allowed) { await auditChannel.emit(tool_call_denied, { traceId: session.traceId, toolName: toolCall.name, reason: policy.reason, source: session.source, }); throw new ToolCallDeniedError(policy.reason); } const redactedParams redactSensitiveFields(toolCall.arguments); const startedAt Date.now(); await auditChannel.emit(tool_call_start, { traceId: session.traceId, toolName: toolCall.name, args: redactedParams, server: toolCall.server, }); try { const result await mcpClient.callTool(toolCall.server, toolCall.name, toolCall.arguments); const redactedResult redactSensitiveResponse(result); await auditChannel.emit(tool_call_end, { traceId: session.traceId, toolName: toolCall.name, resultSummary: summarize(redactedResult), durationMs: Date.now() - startedAt, status: success, }); return result; } catch (err) { await auditChannel.emit(tool_call_end, { traceId: session.traceId, toolName: toolCall.name, error: err.message, durationMs: Date.now() - startedAt, status: error, }); throw err; } }有朋友可能会问既然已经有 MCP 协议标准为什么还要自己写一个引擎层直接在每个 Agent 进程里维护一个 MCP Client 不是更简单吗如果团队只有一两个 Agent确实没必要引入 Harness。但一旦 Agent 数量超过五六个或者不同业务线各自维护一套 MCP 接入代码你会发现审计格式不统一、权限策略各写各的、新接入一个 MCP Server 要改好几个服务。Harness 的价值就在这个规模节点上开始显现它把“接 MCP”这件事收敛成了一处配置把“管调用”收敛成了一处策略。3. MCP 审计方案怎么设计才能落地3.1 审计到底要审什么三个维度讲到审计方案很多人第一反应是“记日志”。审计和日志确实是近亲但需求维度完全不同。日志关注的是系统运行状态审计关注的是“谁在什么时间通过什么方式做了什么操作”。落到 MCP 调用场景我梳理成三个维度。合规审计要回答的问题是是否有权限依据。企业内部尤其是涉及数据权限的业务每一次工具调用都要能对应到具体会话、具体用户、具体授权范围。比如一个内部账单查询工具模型在用户对话中调用了它合规审计就需要能追溯到触发这次调用的用户是谁、这个用户是否在该工具的白名单里。安全审计要回答的问题是是否有异常行为。典型场景包括高频调用、越权尝试、敏感参数外发。比如某个 Agent 在短时间内连续调用了大量外部搜索工具或者工具参数里出现了类密钥字段这些行为应该能被审计规则捕捉并触发告警。链路追踪要回答的问题是出问题时怎么排障。Agent 程序是非确定性的同一个用户的同一个问题两次运行的路径可能完全不同。如果某一个工具返回了错误结果导致模型最终生成了错误答案你需要在审计事件里把“哪一轮对话→哪个子任务→哪个工具调用→什么结果”整条链路拼出来。这三个维度对应到审计事件字段上我总结了下面这张表审计维度核心关注点典型字段示例合规审计谁、何时、依据什么权限调用userId、sessionId、policyId、permissionScope安全审计越权、高频、敏感数据外发clientIp、toolName、argsHash、riskScore、blocked链路追踪调用来源、父子关系、耗时traceId、spanId、parentSpanId、durationMs、status三个维度最好不要分开做三套方案否则复杂度会成倍增加。我的做法是统一生成一种“宽表型”审计事件字段里同时带上身份、策略、链路和结果四个组的信息。这样查询时可以按任意维度过滤不需要维护多套数据结构。3.2 审计事件模型与存储选型审计事件的格式设计是整套方案里最需要提前想清楚的环节。事件模型一旦确定后期改字段成本很高尤其是明细类数据基本没办法无痛迁移。我参考了 Kymo 分享里对调用可观测性的强调结合自己的实践最终固定成了下面这种 JSON 结构{ schemaVersion: 1.0, traceId: d73f2a6c9e0b4f1a, spanId: f01b3c7a82d94e6b, parentSpanId: a9c44e7d12bf5e80, sessionId: chat-20250617-0082, userId: u_100234, source: customer-service-agent, modelName: qwen-plus, toolCall: { server: internal-filesystem, toolName: fs_read_file, argsHash: sha256:5f9d7e..., argsPreview: { path: /workspace/config/demo.txt, encoding: utf8 } }, policy: { allowed: true, matchedRule: allow_user_whitelist }, result: { status: success, summary: file_size842, lines39, durationMs: 86 }, time: 2025-06-17T14:33:21.708Z }存储选型是另一个容易踩坑的地方。网上很多教程直接推荐 Elasticsearch但实际用过会发现审计数据写入量大、保留周期长、更新频率极低用 ES 既贵又重。我更倾向于按数据冷热做分层。下面是我整理的一份选型对照表存储方案适用场景优点缺点本地文件 定时归档最小原型、单机验证零依赖友好排障不支持检索无法支撑生产关系型数据库小规模、每日万次级调用事务可靠查询灵活写入吞吐上限低日志膨胀快ClickHouse中大规模审计分析列式压缩查询快写入高运维成本略高不支持高频单行更新对象存储 归档合规审计、长期冷存便宜无限扩展热查延迟高我在内部环境最终选的是 ClickHouse 接热链路对象存储做冷归档。审计事件先写入消息队列消峰再由消费者批量写入 ClickHouse超过 90 天的分区自动转存对象存储。这个方案的写入吞吐足够支撑每日千万级事件查询一个 traceId 下的完整调用链可以在秒级返回成本也远低于把全量审计丢给 ES。3.3 协议限制、旁路方案与成本控制MCP 审计有个绕不开的难点你只能审计到 Harness 这一层能看到的东西。MCP 协议本身没有定义审计相关能力工具的参数是自由 JSONServer 渲染的结果也是五花八门。更麻烦的是一个 MCP Server 内部可能还会再调用外部 API比如一个搜索工具可能同时查询多个上游数据源这些子调用对 Harness 来说是不可见的。所以设计审计方案时一定要认清边界。Harness 层审计的是“Agent 与 MCP Server 之间”的交互。如果审计目标是“全链路总体追踪”那必须在 MCP Server 内部做埋点或旁路上报。最轻量的做法是让 MCP Server 在收到请求时往日志输出端打一条结构化日志再由收集器统一采集。这个方案侵入性小但对 Server 维护方有要求他们需要愿意配合约定日志格式。另一个现实问题是成本。全量审计事件很容易在流量高峰时膨胀成海量数据。我的经验是不要一刀切全量或全不录而是按风险等级划分采样策略。高风险工具如删除类、写入类、外发类必须 100% 全量审计中风险工具如文件读取、数据查询可以全量记录摘要但不记录参数明细低风险工具如随机数生成、时间查询做动态采样比如保留前 20% 的调用明细。这样可以控制存储成本同时保证安全审计的核心覆盖。4. 实操复盘从零搭一个最小审计链路4.1 组件清单与部署方式理论讲多了容易飘我直接把我复现的最小链路完整列出来。这套方案跑在我本地一台 8 核 16G 的 Linux 机器上用 Docker Compose 编排整体结构包含四个组件。Agent Demo一个极简对话服务负责把用户问题交给大模型模型返回工具调用后提交给 Harness SDKHarness 服务核心控制层维护 MCP Client、执行策略、生成审计事件MCP 测试 Server我用了两个本地 Server一个模拟文件系统工具一个模拟外部搜索工具方便制造“调用成功”和“调用失败”两类场景审计存储先用 MySQL 验证模型确认字段没问题后换成 ClickHouse 跑压测。组件之间的调用链是Agent Demo 收到模型决定 → 调用 Harness SDK → SDK 走策略检查 → 转发 MCP Server → 返回结果 → 生成审计事件 → 写入审计存储。整个链路里 Harness 是唯一必经节点这个定位在我复现过程中验证了一次又一次只要请求不是刻意绕过 Harness审计事件就是完整的。4.2 核心代码路径拦截一次 MCP 工具调用代码路径的核心在上文已经给出这里补充一个更完整的工程视角。我在 Harness SDK 里把处理逻辑拆成了四个阶段。初始化阶段读取配置、连接 MCP Server、拉取工具目录、构建策略索引。注意工具目录一定要做本地缓存否则每次调用前都去远程 Server 执行tools/list会额外增加 200ms 到 500ms 延迟。我实测在本地环境缓存工具目录之后单次调用平均延迟下降了约 40%。评估阶段对一次工具调用执行三层检查。身份层看调用来源会话是否有效工具层看该工具是否对当前用户开放参数层看参数是否包含敏感字段或超出允许范围。三层任一不通过则拒绝调用并写事件不进入转发。转发阶段调用 MCP Client 把请求发给目标 Server。这里要特别注意MCP Client 不要每次新建连接最好是连接池复用否则工具密集调用时握手开销会拖垮吞吐。收尾阶段拿到结果后生成摘要、脱敏、封装审计事件通过异步队列发送避免阻塞主链路。我在本地实测异步发送审计事件后单次工具调用的 P99 延迟比同步写日志的方式降低了约 65%。4.3 脱敏、白名单与权限控制的落地配置审计方案里最容易出安全事故的地方恰恰是审计本身。如果审计事件原样记录 Agent 传给工具的参数那么模型在对话中提到的用户手机号、密钥片段、内部地址都会原样进入审计库一旦审计库被读取反而造成更严重的数据泄露。所以脱敏必须在 Harness 层强制生效而不是依赖存储端的过滤逻辑。我给 Harness 配了一份简单的脱敏规则{ sensitive_patterns: [ password, token, secret, authorization, api_key, access_key, phone_number, (?![A-Za-z0-9])[A-Za-z0-9]{32}(?![A-Za-z0-9]) ], redact_mode: replace, placeholder: *** }这份规则的意思是字段名命中敏感关键词的直接替换为占位符正文里匹配到 32 位连续字母数字的类密钥内容也替换为占位符。规则本身跑起来很简单真正要留意的是不匹配的情况。比如一个工具参数是 JSON 字符串敏感字段嵌套在内部字段名匹配还要加上深度递归扫描不能只看顶层。权限控制方面我给工具目录打上了安全等级标签并配了一个最小白名单策略。安全等级为 high 的工具用户必须属于显式白名单等级为 medium 的工具默认开放但要求会话来源是内部系统等级为 low 的工具对所有经过身份校验的会话开放。工具级白名单之外参数级控制也需要一套规则。比如文件读取工具允许读/workspace/project-a下的文件但不允许读/etc/passwd。参数级校验不做的话工具级白名单很容易被绕过——模型只要构造一个越界的路径参数工具照样执行。我在 Harness 里对每个高敏工具配置了参数校验器使用的是 JSON-Schema 的 pattern 和 enum 约束不需要硬编码逻辑配置驱动即可。5. 常见问题与排查技巧实录5.1 一张问题速查表复现和研究过程中我遇到了一堆实际问题。这里整理成一张速查表按现象、可能原因、排查方法、处理建议四列给出方便你直接对照排查。现象可能原因排查方法处理建议工具调用一直超时MCP Server 单次执行耗时长未做超时控制查看 Server 端日志对比耗时分布在 Harness 层增加 timeout 配置对长耗时工具做异步化审计事件里敏感字段仍出现脱敏发生在落库阶段而不是 Harness 层检查脱敏代码位置确认在事件封装前执行把脱敏逻辑前移到调用结束后立即执行多个会话互相串线traceId 和 sessionId 映射关系没维护好打开全链路日志检查会话上下文传递在会话初始化时固定 traceId子任务统一继承MCP Server 返回 invalid schemaAgent 生成的参数与 OpenAPI 不一致抓取请求原文对比 JSON-Schema在转发前做参数清洗丢弃多余字段高频调用导致存储膨胀全量审计没有做分级采样查看单日审计事件总量按风险等级分层采样冷数据定时转存某一个工具调用被莫名拒绝参数级白名单冲突查看策略引擎日志中的匹配规则调整校验器的优先级顺序加测试用例覆盖5.2 几个我反复踩的坑第一个坑是审计事件丢了溯源信息。最开始我只记录 toolName、参数和结果没有记录是哪一轮对话的子任务触发的调用。结果等到实际排障时看到一条异常调用记录完全没有上下文根本不知道模型为什么会在那个时点调用这个工具。后来我在事件模型里强制要求带上 parentSpanId 和 sessionId并且每次 Agent 子任务开始时都生成新的 spanId再通过 parentSpanId 挂到总链路上排障效率才真正提上来。第二个坑是 MCP Server 的 transport 兼容。我有两个本地测试 Server一个走 stdio、一个走 HTTP最开始为了省事都统一用 HTTP 配置结果 stdio 那个服务反复启动失败。后来确认是 MCP SDK 的初始化方式不同导致的stdio transport 要在本地拉起子进程并管理生命周期HTTP transport 则是建立远端连接两者的连接池管理和超时配置完全不是一套逻辑。这提醒我Harness 的配置里必须显式区分 transport 类型不能默认所有 Server 都支持同一种协议。第三个坑是审计事件写库阻塞了主链路。我最初实现审计写入时是同步写 MySQL结果在调用频率高的时候Agent 响应变慢模型开始频繁报错。后来改为“先落本地缓冲再异步批量上报”之后问题才彻底解决。这是个很老生常谈的性能建议但在审计场景下特别容易被忽略因为审计往往被当成“边角料逻辑”没有多少人会认真设计它的异步机制。第四个坑是策略规则默认放行带来的“假安全感”。我一开始写的是白名单开白默认放行所有未匹配策略的工具调用。结果是某个新接入的工具因为忘了配规则直接对所有人开放。后来我把策略引擎的默认行为从“allow”改成了“deny”凡是匹配不到明确规则的调用一律拒绝。这个改动虽然让前期接入成本变高了但安全收益非常明显。默认拒绝应该成为 Harness 策略引擎的核心设计原则。6. 研究完之后我自己的几点体会把 Kymo 的分享和这几天的复现连起来看我最想通的一件事是Harness 引擎本质上不是要把 Agent 关进笼子而是给它在企业内部划一条可信跑道。Agent 的能力边界不是靠限制模型参数画出来的而是靠工具调用前的策略判断画出来的。模型可以自由思考但每一次动手都必须经过授权、记录、可追责这套逻辑对于企业内部助手类的 Agent 几乎是刚需。MCP 审计方案则是这条跑道的“黑匣子”。没有审计权限控制做得再好也缺少事后纠偏的依据。我在实际使用中发现审计数据最值钱的应用场景反而不是“安全追责”而是反哺策略和模型——当你把被拒绝的调用、异常的参数、超时的工具统计到一起就能看到 Agent 在真实场景里到底渴望哪些能力、哪个工具的 Schema 设计得让模型困惑。这些信息对优化 Agent 应用的价值比单纯查日志高得多。最后再分享一个小建议如果你们团队刚开始做 Agent不要一上来就自研完整的 Harness 引擎和审计平台。先找一个统一的调用入口哪怕只是一个很薄的中间层把 MCP Client 集中到一个服务里审计事件先打本地日志文件。等调用量上来、策略需求变复杂了再逐步往 ClickHouse、策略引擎、默认拒绝那套方向演进。我这次复现就是从最小链路起步的最终跑通之后反而觉得“少即是多”这套思路在 Agent 基础设施里同样适用。