MCP Server 上线之后:怎么知道模型都在拿你的工具干什么

发布时间:2026/10/18 8:05:43
MCP Server 上线之后:怎么知道模型都在拿你的工具干什么
MCP Server 上线之后怎么知道模型都在拿你的工具干什么前言上一篇聊 MCP Server 的测试结尾留了个钩子可观测性。这篇把它补上。先描述一个上线后最常见的场景用户反馈AI 助手答非所问你怀疑是模型乱调了工具但手头没有任何记录——不知道那次会话里模型看到了哪些工具、发了什么参数、工具返回了什么。你唯一能做的是在自己电脑上连一遍复现看看。感觉修好了但你其实说不清刚才那次的真实原因。传统 Web 服务有成熟的可观测性方案日志、指标、链路追踪三大件。MCP Server 大体也能套用但有两个本质差异直接决定了方案怎么做调用方是模型参数是模型生成的。参数里可能带着用户的敏感数据(文件路径、查询语句、邮件内容)日志里全量打印参数等于把用户数据抄了一份到日志文件——你自己制造了一个泄露面一次模型决策可能触发一串工具调用。链路的根不在 HTTP 入口而在某轮对话里传统按请求串 trace 的思路需要调整。这篇文章讲我落地的做法协议内日志、结构化调用日志、按工具分维度的指标、可回放的审计记录。代码仍是 TypeScript 官方 SDK。一、先分清两条通道协议内日志 vs 本地日志MCP 协议自带 logging 能力这是很多人没注意到的。Server 声明capabilities.logging之后可以随时向客户端推日志消息const server new Server( { name: todo-server, version: 1.0.0 }, { capabilities: { tools: {}, logging: {} } }, ); // 工具执行中向客户端/用户报告进度和状态 await server.sendLoggingMessage({ level: info, logger: todo-server, data: 已扫描 120 个文件开始匹配关键词, });客户端(如果实现了 logging)会把消息展示给用户。这适合放过程可见的信息长任务做到哪一步、准备批量处理什么。对用户来说工具不再是按下回车后黑箱三分钟。但要清楚它的边界协议内日志是给这次会话的用户看的不可检索、不会持久化(取决于客户端)、你自己的排查也用不上。所以本地结构化日志同样要做两者不互相替代协议内日志面向用户报进度、报状态本地日志面向你自己记录完整调用事实供排查和审计。还有一个 stdio 特有的约束测试篇提过再强调一次stdio 模式下 stdout 是协议通道本地日志只能写 stderr 或文件。日志框架配置错目的地是 stdio server 的头号自毁方式。二、工具调用日志记什么、不记什么核心原则一次 tools/call 记一行结构化日志够排查问题但不复制用户数据。type ToolCallLog { ts: string; // ISO 时间 sessionId: string; // 会话标识(自增即可) tool: string; // 工具名 argDigest: string; // 参数摘要,不是参数全文 outcome: ok | tool_error | thrown; durationMs: number; resultDigest?: string; // 结果摘要 };关键设计是argDigest(参数摘要)function digestArgs(args: Recordstring, unknown): string { return Object.entries(args) .map(([k, v]) { const s String(v); const shown s.length 24 ? s.slice(0, 24) …(${s.length}) : s; return ${k}${shown}; }) .join(); }记参数的形状而不记内容路径记到文件名、查询记前 24 个字符加长度。排查模型传了空参数参数类型不对这类问题完全够用同时避免把用户邮件正文、数据库记录写进日志。如果某个工具确实需要完整参数才能排查做成开关配置 默认关闭并在文档里写明这会记录用户内容。用装饰器把记录逻辑统一包住所有工具避免每个 handler 里手写const timed T( name: string, fn: (args: Recordstring, unknown) PromiseT ) async (args: Recordstring, unknown) { const start Date.now(); let outcome: ToolCallLog[outcome] ok; try { return await fn(args); } catch (e) { outcome thrown; throw e; } finally { log.info({ tool: name, argDigest: digestArgs(args), outcome, durationMs: Date.now() - start, }); } };注意outcome只在catch里改标记、finally里统一落日志——isError形式的业务失败不抛异常会在正常返回路径记录想区分的话在返回前检查一次result.isError把它标成tool_error。三、指标按工具名分维度别只看平均值有了结构化日志指标就是聚合问题。最少要这三个全部按工具名分维度调用次数哪个工具被用得多直接指导优化优先级错误率tool_error thrown / total。某个工具错误率突然抬升往往是依赖的外部服务在抖动耗时分布:P50 和 P99,不要只看平均值。为什么 P99 特别重要模型客户端普遍对工具调用有超时慢工具会触发模型侧重试重试又把负载打回去形成恶性循环。平均值 200ms 的工具如果 P99 是 8 秒用户看到的就是经常转圈失败而你的平均值监控一片绿。实践里我给每个工具设一个软预算(比如 3 秒)接近预算就先推协议内日志(正在查询更多数据…)让用户和模型都知道工具还活着。四、审计日志为出事之后留证据《MCP 权限模型》那篇讲过工具粒度的授权需要知道谁在什么上下文里调了什么。落到实现就是审计日志和第二节的运行日志相比它多了两个诉求身份哪个客户端、哪个(如果有的话)终端用户。stdio 本地场景身份意义不大HTTP/远程部署时这列是命根子不可变性审计记录写独立的 append-only 文件(或直接进日志服务)和应用日志分开避免排查时顺手改两行。审计事件的最小集:时间、身份、工具名、参数摘要、结果状态、会话 ID。和运行日志几乎同构——所以我直接复用同一套 digest 逻辑只是输出到不同 sink。每条记录加一行换行符分隔的 JSON(NDJSON)出安全事件时grep 工具名就能拉出完整时间线。给一个务实的边界建议单机个人工具不必上审计运行日志足够凡是连了公司数据源、有多个使用者的 server审计从第一天就要有——它不能事后补。五、一条端到端的排查路径最后把工具串起来。收到AI 回答不对的反馈时拿会话时间点在运行日志里按sessionId过滤看模型实际调了哪些工具、参数摘要是什么形状参数明显异常(空参、类型不对)→ 回到工具 schema 和描述八成是描述有歧义模型在猜参数正常但结果tool_error→ 工具自身问题看错误率和下游依赖调用都正常、答案仍错 → 才是模型侧的问题(上下文、prompt)此时你才需要动 prompt。这条路径的价值在于把玄学问题变成分层排查先工具层再模型层而不是对着聊天记录猜。写在最后清单版总结给要上线的 MCP Server协议内 logging:长任务报进度给用户看本地结构化日志一行一次调用参数记摘要不记全文stdio 只写 stderr/文件指标按工具名分维度盯错误率和 P99,不只看平均多人/连公司数据的 server,第一天就开 append-only 审计排查顺序工具参数 → 工具结果 → 才轮到模型。到这里MCP Server 的工程化三篇(测试、可观测性加上安全四篇)就齐了。下一篇可能聊聊部署形态的选择stdio 子进程、本地 HTTP 和远程托管各自适合什么场景。