生产化与复盘:给工具写评估、版本化、发布到 npm 与 registry、可观测与综合项目
把一个能跑的服务端变成一个能长期维护的服务端:给工具写评估集、把描述改动当成破坏性变更来管、补上追踪与指标,最后把这一周串成一个作品集条目。
今日目标
- 能为一个工具写出可重跑的评估集,并说出评估该覆盖哪三类用例
- 能判断一次改动是不是破坏性的,并说明工具描述改动为什么也算
- 能给服务端补上追踪与关键指标,并说出出问题时先看哪一个
最后一天。前六天你手上已经有一个能跑、能远程、能被自己的客户端连上、并且过了一遍安全清单的服务端。今天回答最后一个问题:它怎么活过接下来的半年。 读完回来把上面三条勾掉。
小白版讲解
工具也要评估
先看一个真实到有点无聊的故障。
某个服务端上线三个月,工具没改一行代码,某天有人把 search_docs 的描述从「在内部文档库里按关键词搜索」改成了「搜索文档」——理由是「太啰嗦了」。上线之后没有任何报警:单元测试全绿,服务端零错误,日志干干净净。两周后有人反馈「这个助手最近变笨了,问它文档它老说不知道」。
原因是模型不再选这个工具了。它一次都没调用失败过,因为它压根没被调用。
这就是 MCP 工具最反直觉的一点:「模型选不选得中」比「工具本身对不对」更早出问题,而前者没有任何传统测试能覆盖。 单元测试测的是「给定参数,输出对不对」;描述改坏了,参数根本到不了你的函数。
所以工具需要另一层测试,业内一般叫评估(eval)。它的形状很简单:给一句用户的话和一张工具表,看模型选了哪个(或者一个都没选),拿它和期望值比。
评估集怎么写
评估集只有一个设计要点:三类用例缺一不可。
| 类别 | 测什么 | 建议占比 |
|---|---|---|
| 正例 | 意图明确时选得中 | 四成 |
| 边界 | 意图靠语义而不是关键词时还选得中 | 三成 |
| 诱导误选 | 不该调的时候一个都不调 | 三成 |
前两类大家都会写,第三类是分水岭。
只有正例的评估集会给你一个 100% 的假象:它测不出「不该调的时候调了」,而线上大多数用户投诉恰恰是这一种——用户随口说了句「谢谢,不用查文档了」,助手转头就去查了一遍文档。这叫过度触发,它比漏调更伤体验,因为它有副作用。
诱导误选的用例怎么挑?找话题沾边但意图不是的说法。「谢谢,不用查文档了」(有「查」有「文档」,意图是否定)、「文档这个词英文怎么说」(有「文档」,是个语言问题)、「你都能干什么」(闲聊,调工具就是错)。
然后是断言。断言只有一条,但两个方向都要判:
// 期待某个工具时,选中它才算过;期待「不调用」时,什么都不选才算过。
// 把 null 当成「随便都行」是评估集最常见的写法错误——它让负例永远绿,
// 比没有负例更糟,因为你以为自己测了。
export function judge(expected: string | null, actual: string | null): boolean {
return expected === null ? actual === null : actual === expected
}
// 三类缺一不可,而且要用 throw 而不是打一行警告:
// 评估集是防线,防线坏了必须停下来
export function loadCases(raw: { cases: EvalCase[] }): EvalCase[] {
for (const kind of ['happy', 'edge', 'trap'] as const) {
if (!raw.cases.some((c) => c.kind === kind)) throw new Error(`评估集缺少 ${kind} 类用例`)
}
return raw.cases
}# 期待某个工具时,选中它才算过;期待「不调用」时,什么都不选才算过。
# 把 None 当成「随便都行」是评估集最常见的写法错误——它让负例永远绿,
# 比没有负例更糟,因为你以为自己测了。
def judge(expected: str | None, actual: str | None) -> bool:
return actual is None if expected is None else actual == expected
# 三类缺一不可,而且要抛异常而不是打一行警告:
# 评估集是防线,防线坏了必须停下来
def load_cases(raw: dict) -> list[EvalCase]:
cases = raw["cases"]
for kind in ("happy", "edge", "trap"):
if not any(c["kind"] == kind for c in cases):
raise ValueError(f"评估集缺少 {kind} 类用例")
return cases一条实践建议:评估集要能一条命令跑完,并且跑一次别超过一分钟。 跑得慢的评估集等于没有评估集,因为改描述的人不会等。用一个便宜的小模型跑,比如 claude-haiku-4-5-20251001,选工具这件事不需要最贵的那个。
版本化:改一句描述比改一个字段名更危险
先看协议自己怎么做版本。MCP 的版本号是 YYYY-MM-DD 形式,含义很特别:它标的是最后一次做破坏性变更的日期。向后兼容的改动不会让版本号往前走,所以同一个版本号在一段时间内是可以继续演进的。弃用也有明文政策:被标记为弃用的特性会留在规范里至少十二个月(走加急通道也要至少九十天)才可能被移除。
这套做法值得抄。你的服务端也该有一份「什么算破坏性」的清单,官方在讲扩展演进时给的定义直接可用:删除或重命名字段、改字段类型、改变现有行为的语义、新增必填字段,这四类都是破坏性的。
但工具比普通 API 多一类,而且是最容易被漏掉的那类:
改一句描述,就是一次行为变更。
理由前面那个故障已经说清了:描述是模型选工具的唯一依据。改字段名会让调用方立刻报错——错误是响亮的,五分钟内就有人来找你。改描述不会让任何东西报错,它只会让线上选中率悄悄掉几个点,两周后以「最近变笨了」的形式浮上来。响亮的错误比安静的退化好处理得多,所以描述改动反而更需要闸门。
闸门就是上一节那份评估集:描述改了必须跑一遍,选中率掉了就别合。
字段层面的兼容技巧和普通 API 一样,但有两条 MCP 特有的:
- 加字段要加成可选的,因为老客户端不会传新参数。
- 要改语义就换个工具名,别原地改。旧的标成弃用、描述里写明替代品,留一段时间再删——你不知道有多少人的提示词里写死了那个名字。
顺带说扩展的做法,思路一致:需要变更时优先用能力标志或者扩展设置里的版本字段,实在避不开破坏性变更就换一个新的扩展标识符。
发布:npm 与注册表是两件事
先厘清一个多数人第一次会搞错的关系:官方注册表只存元数据,不存产物。
产物住在包注册表——npm、PyPI、Docker Hub。官方 MCP 注册表存的是一份 server.json:这个服务端叫什么、去哪儿找它(哪个 npm 包、或者哪个远程地址)、怎么启动它、需要哪些环境变量。它的定位是给下游聚合器(各种 MCP 市场)消费的,而不是给宿主直接查的。
所以发布是先发包,再登记,顺序不能反。
三样东西必须对齐,错一个就会被拒收:
| 位置 | 字段 | 约束 |
|---|---|---|
package.json | mcpName | 必须等于 server.json 的 name |
server.json | name | 反向域名加斜杠,如 io.github.用户名/docs |
server.json | version | 唯一、发布后不可改、不能是版本范围 |
命名空间是靠所有权验证的:用 GitHub 认证时,名字必须以 io.github.你的用户名/ 开头;想用自己的域名就走 DNS 验证。这也是注册表防冒名的主要手段。
版本那条要特别小心:同一个版本号只能发一次,发布之后元数据不能改。打错一个字只能往上加一个版本。而且 ^1.2.3、1.x、>=1.2.3 这类看起来像范围的字符串会被直接拒收——这是刻意的防呆。推荐语义化版本,本地服务端还应当让 server.json 的版本与包版本对齐,免得两边对不上号。
包本身也有几条别忘:要有可执行入口(bin),不然用户没法用 npx 起你的服务端;files 里要包含产物目录;README 第一屏要写清「它是什么、怎么装、要哪些环境变量」——读者预算是三分钟,而且他不打算 clone 下来跑。
可观测:先看错误率,再看选中率
服务端上线之后,你需要能回答的问题就那么几个。指标少而准,比一堆没人看的曲线有用。
四个指标够用:
- 按工具的调用次数——哪个工具是主力,哪个从来没被调过(没被调过的工具要么该删,要么描述有问题)。
- 按工具的错误率——注意要把两类错误分开数:协议错误是你的 bug,工具执行错误是模型给错了参数。后者持续偏高往往不是模型笨,是你的 schema 描述没说清。
- 按工具的耗时分布——看 P95 不看平均值。远程服务端上,一个工具从 200 毫秒退化到 8 秒,平均值可能只动一点点。
- 选中率——就是评估集的分数,定期跑,当成一条曲线看。
排查顺序也固定:先看错误率,再看选中率。 错误率正常但用户说不好用,八成是选不中;错误率飙了才去看代码和上游。
追踪这一层,协议给了位置但没给规矩:请求的 _meta 是一个开放字段,键名必须带前缀(反向域名,和扩展标识符同一套命名规则)。所以把追踪上下文塞进一个自己的 _meta 键里透传,是合规且通用的做法。
// 客户端:把追踪上下文塞进自己命名空间下的 _meta 键
// 键名必须带前缀(反向域名),这和扩展标识符是同一套命名规则
const TRACE_KEY = 'com.example/trace'
function withTrace(params: Record<string, unknown>, traceparent: string) {
return { ...params, _meta: { ...(params._meta as object), [TRACE_KEY]: { traceparent } } }
}
// 服务端:取出来接上自己的 span;取不到就自己开一条,别因为缺字段就报错
function startSpan(params: Record<string, unknown>, toolName: string) {
const meta = params._meta as Record<string, { traceparent?: string }> | undefined
const parent = meta?.[TRACE_KEY]?.traceparent
return tracer.startSpan(`tools/call ${toolName}`, { parent })
}# 客户端:把追踪上下文塞进自己命名空间下的 _meta 键
# 键名必须带前缀(反向域名),这和扩展标识符是同一套命名规则
TRACE_KEY = "com.example/trace"
def with_trace(params: dict, traceparent: str) -> dict:
meta = {**params.get("_meta", {}), TRACE_KEY: {"traceparent": traceparent}}
return {**params, "_meta": meta}
# 服务端:取出来接上自己的 span;取不到就自己开一条,别因为缺字段就报错
def start_span(params: dict, tool_name: str):
parent = params.get("_meta", {}).get(TRACE_KEY, {}).get("traceparent")
return tracer.start_span(f"tools/call {tool_name}", parent=parent)最后一条容易踩的:stdio 上服务端的日志只能走 stderr。想看指标就另开一个 HTTP 端点,别往 stdout 上打——那里只能放协议消息,多一个字客户端就解析失败。
扩展生态:核心之外的三块
规范之外还有一层叫扩展。它们是可选的,用 io.modelcontextprotocol/ 这样的反向域名做标识符,在双方的能力声明里协商,默认关闭,必须显式启用。一方支持另一方不支持时,支持的那方要么回落到核心行为,要么明确报错。
官方目前有三块,各解决一件核心协议不管的事:
- 授权扩展:机器到机器的客户端凭证流程,以及面向企业的集中式访问控制。核心的 OAuth 那一套假设有个用户坐在浏览器前面,这两个补的是「没有用户」和「有 IT 管理员」的场景。
- 应用界面(MCP Apps):让服务端在宿主里渲染交互式界面——图表、表单、播放器。工具返回一段文字终究有极限。
- 任务(Tasks):长时间运行的异步任务,带轮询、中途补充输入和持久句柄。一个跑二十分钟的活,不该占着一条请求。
还有一块在路上:把技能(skills)搬到 MCP 上分发,目前是一个工作组,还没有成为已发布的官方扩展。想现在就用技能,走的仍然是它自己那套目录约定——那是另一门课的主场。
判断要不要碰扩展,一句话:核心协议解决不了、而你确实遇到了这个问题的时候再看。 扩展默认关闭是有道理的,每开一个就多一层兼容负担。
复盘:这一周你手上有什么
先把这门课放回坐标系里。整个 Agent 工程有三件事经常被混为一谈,一句话分开:
MCP 管接线,Skills 管经验,上下文工程管取舍。
MCP 解决「工具和数据怎么接进来」;Agent Skills 解决「把做事的经验做成可复用的能力」;上下文工程 解决「什么该进上下文窗口」。三者不冲突,也互相替代不了。这一周你只学了第一件。
然后清点战利品。这七天下来,你手上应该有这些东西:
| 天 | 产出 | 能写进简历的那一句 |
|---|---|---|
| D1 | 逐字段标注的会话报文 | 能读懂协议,不是只会调 SDK |
| D2 | 天气与汇率 stdio 服务端 | 会设计 schema 与工具描述 |
| D3 | 笔记库资源服务端 | 会做 URI 模板、分页与通知 |
| D4 | Streamable HTTP 服务端加容器部署 | 手写过远程绑定,理解无状态取舍 |
| D5 | 多服务端聚合的手写客户端 | 两侧都写过,知道边界在哪 |
| D6 | 安全检查清单与一次攻击复现 | 能说清提示注入与混淆代理 |
| D7 | 可发布模板加评估脚本 | 有生产化意识,不止能跑 |
串成作品集的建议是:把 D4 的服务端、D5 的客户端、D7 的评估与发布配置合成一个仓库,README 里放一张架构图、一段三条命令内跑起来的快速开始、一节「关键设计决策」。最后那一节是唯一抄不来的部分,也是面试官挑追问的地方——每条决策都要说得出放弃了什么。
至于每个数字,只写你自己跑出来的,并注明测量条件。「本机两副本、离线模式、9 项自测全绿」朴素但站得住;写一句没跑过的规模数,作废的不只是那一条,是你其余全部的真数字。
源码导读
动手实验
模板里的三个工具是刻意做得容易混淆的:搜索、按 slug 读、新建。它们之间的界线全靠描述那几句话划,正好给评估集当靶子。starter 跑出来是 3 项绿 4 项红。
- 先跑一次 solution 的评估,看跑分表长什么样,特别看诱导误选那三条为什么该判"不调用"。
- 在 starter 里先修 judge 的断言,再去补负例——顺序反了的话你补的负例会全部假绿。
- 给服务端的 tools/call 分支补上三条路径的指标,别漏掉 isError 为真那一条。
- 补全发布检查,然后故意把 mcpName 改坏一次,确认它真的报得出来。
- 最后跑一次 pnpm build 和 npm pack --dry-run,看清单里有没有 dist 与 server.json。
有 key 的话,去掉 MOCK=1 用真模型再跑一次评估,然后改一句工具描述再跑一次——这一下你会立刻明白为什么描述改动要算破坏性变更。
面试题
今天 3 道题在下方题库区,侧重工具评估的设计、什么样的改动算破坏性变更、以及线上排查的顺序。展开后先看"分析过程"再看要点——照着推导练,比背要点管用。标注"国内高频 / 海外高频"方便按目标市场取舍。
检查清单与明日预告
- 能为一个工具写出可重跑的评估集,并说出评估该覆盖哪三类用例
- 能判断一次改动是不是破坏性的,并说明工具描述改动为什么也算
- 能给服务端补上追踪与关键指标,并说出出问题时先看哪一个
- 能说清 npm 与官方注册表的分工,以及三处必须对齐的字段
- 实验的 5 条验收标准全部通过
- 3 道面试题不看要点也能答出至少 2 道
七天到这里结束。回头看第 1 天那个问题——「那些工具凭什么能被别人的程序也用上」——你现在的答案已经不是一句话,而是一个能跑的服务端、一个能连的客户端、一份填过的安全清单和一套能拦住退化的评估。
接下来往哪走,看你缺哪一块:想把「怎么做事」的经验也变成可复用的东西,去 Agent Skills;觉得工具接多了上下文不够用,去上下文工程。接线、经验、取舍,三块凑齐,你才算真的会搭 Agent。
面试题库
怎么评估一个 MCP 工具做得好不好?你会设计哪几类测试用例?How do you evaluate whether an MCP tool is any good, and what categories of test cases would you design?
国内高频海外高频进阶#eval#tooling分析过程 · 先想清楚再作答
- 这题在筛「有没有真的上线过工具」。答「写单元测试」是答错了赛道——单元测试测的是给定参数输出对不对,而 MCP 工具最先出问题的地方是模型压根没选它,参数根本到不了你的函数。
- 先把要评估的对象说清:评估评的是**选中率**,也就是给一句用户的话和一张工具表,模型会不会选中该选的那个。工具本身的正确性归单元测试,两层不要混。
- 然后给三类用例,这是本题的正面回答。正例:意图明确时选得中,比如「帮我搜一下发布流程文档」。边界:意图靠语义而不是关键词,比如「release-process 这篇讲了什么」——没有任何动词提示,只有一个像 slug 的词,最容易被误判成搜索。诱导误选(负例):不该调的时候一个都不调,比如「谢谢,不用查文档了」「文档这个词英文怎么说」「你都能干什么」。占比我一般给四三三。
- 第三类是分水岭,要主动强调:只有正例的评估集会给你一个 100% 的假象,它测不出过度触发,而线上大多数投诉恰恰是过度触发——用户随口一句否定,助手转头就去干了,还带副作用。
- 接着讲断言,这里有个最常见的写法错误:expected 为 null 的用例被当成「随便都行」,于是负例永远绿。正确的断言两个方向都判:期待某个工具时选中它才算过,期待不调用时什么都不选才算过。我会额外拿一个「总是选同一个工具」的假选择器再跑一遍,要求负例全部失败——这验的不是选择器,是我的断言真的在起作用。
- 最后是工程约束:评估集要能一条命令跑完,一分钟以内,用便宜的小模型跑。跑得慢的评估集等于没有,因为改描述的人不会等。结果也不要只看总分,按三类分开看才有行动价值:正例掉了说明描述写糊了,边界掉了说明缺了区分相似工具的那句话,负例掉了说明描述写得太热情。
- 可预期的追问:什么时候跑?描述、schema、工具增删这三类改动都必须跑,把它挂进 CI。再追问会问到「模型换了怎么办」,答案是评估集是跨模型的资产,换模型时先跑一遍拿到基线,这也是它值得投入的原因之一。
How to reason about it · think before answering
- The screen is whether you have shipped tools. 'Write unit tests' answers the wrong question: unit tests check that given arguments produce the right output, while the first thing to break on an MCP tool is the model not selecting it at all, so the arguments never reach your function.
- Define the target: an eval measures selection accuracy, meaning given a user utterance and a tool list, does the model pick the right tool. Correctness of the tool itself belongs to unit tests, and the two layers should not be blurred.
- Then the three categories, which is the direct answer. Happy path: unambiguous intent, such as 'find me the release process doc'. Edge: intent carried by semantics rather than keywords, such as 'what does release-process say', which has no verb cue and only a slug-shaped token, and is most often misread as a search. Traps: cases where nothing should be called, such as 'thanks, no need to look it up', 'how do you say document in English', and 'what can you do'. I usually weight them four, three, three.
- Stress that the third category is the dividing line: an eval set of only happy paths reports a comfortable hundred percent while measuring nothing about over-triggering, which is what most production complaints actually are, and over-triggering has side effects.
- Then the assertion, where the classic bug lives: treating an expected value of null as 'anything goes', which makes negatives permanently green. The correct assertion judges both directions. I also rerun the whole set with a deliberately wrong selector that always picks the same tool and require every negative to fail, which validates the assertion rather than the selector.
- Finally the engineering constraints: one command, under a minute, on a cheap small model, because a slow eval is no eval, since whoever edits a description will not wait. And report per category rather than one number: happy-path drops mean a vague description, edge drops mean the sentence distinguishing two similar tools is missing, trap drops mean the description over-claims.
- Likely follow-ups: when do you run it? On any change to descriptions, schemas, or the tool set, wired into CI. And what if the model changes? The eval set is a cross-model asset, so you rebaseline on a model switch, which is part of why it pays for itself.
答题要点
- 评估评的是选中率,不是工具正确性;后者归单元测试,两层不能混
- 三类用例缺一不可:正例、边界、诱导误选,建议四三三
- 断言两个方向都判:期待 null 时必须什么都不选;再用故意选错的选择器验证断言本身
- 一条命令一分钟内跑完,按类别分开看分数,描述与 schema 改动必须触发
Key points
- Evals measure selection accuracy, not tool correctness; the latter is unit-tested and the layers must not blur
- Three categories are mandatory: happy path, edge, and traps, weighted roughly four three three
- Assert both directions: an expected null must mean nothing was called, and validate the assertion with a deliberately wrong selector
- One command, under a minute, scored per category, and triggered by any description or schema change
给一个 MCP 服务端做版本管理时,什么样的改动算破坏性变更?为什么说改一句工具描述比改一个字段名更危险?When versioning an MCP server, what counts as a breaking change, and why is editing a tool description more dangerous than renaming a field?
国内高频海外高频进阶#versioning#tooling分析过程 · 先想清楚再作答
- 这题的前半句是常识题,后半句才是筛子。能把「描述也是接口」说明白的人,基本都真的运维过工具。
- 先答常规的四类,官方在讲扩展演进时给过定义,直接可用:删除或重命名字段、改字段类型、改变现有行为的语义、新增必填字段。这四类的共同点是会让已有实现直接失败或者行为不正确。
- 然后补 MCP 特有的第五类:**改工具描述**。理由是描述是模型选工具的唯一依据,改一句描述就是一次行为变更。举个具体的:某个服务端把描述从「在内部文档库里按关键词搜索」精简成「搜索文档」,代码一行没动,两周后用户反馈助手变笨了——模型不再选它了。
- 接着讲为什么它**更**危险,这是题眼:改字段名会让调用方立刻报错,错误是响亮的,五分钟内就有人来找你;改描述不报任何错,单元测试全绿、服务端零错误、日志干净,它只会让选中率悄悄掉几个点,最后以「最近变笨了」这种没法定位的形式浮上来。响亮的错误比安静的退化好处理得多,所以描述改动反而更需要闸门。
- 闸门是什么要说出来:一份三类齐全的评估集,描述改了必须跑一遍,选中率掉了就别合。这也是评估集要能一条命令快速跑完的原因。
- 顺带把兼容技巧补上:加字段要加成可选的,因为老客户端不会传新参数;要改语义就换个工具名而不是原地改,旧的标弃用、描述里写明替代品、留一段时间再删,因为你不知道多少人的提示词里写死了那个名字。
- 可预期的追问一:协议自己怎么做版本?MCP 用 YYYY-MM-DD,标的是最后一次破坏性变更的日期,向后兼容的改动不递增版本;弃用的特性至少保留十二个月才可能移除。追问二:发到注册表之后怎么改?改不了——版本号唯一且发布后元数据不可变,打错字只能往上加一个版本,而且范围形式的版本号会被直接拒收。
How to reason about it · think before answering
- The first half is common knowledge; the second half is the filter. Anyone who can explain that the description is part of the interface has actually operated tools in production.
- Give the four conventional categories, which the official guidance on extension evolution defines directly: removing or renaming fields, changing field types, altering the semantics of existing behavior, and adding new required fields. All four make existing implementations fail or behave incorrectly.
- Then add the MCP-specific fifth: editing a tool description. The description is the model's only basis for selecting a tool, so changing a sentence is a behavior change. Concretely, a server shortened 'search the internal doc library by keyword' to 'search documents', shipped no code changes, and two weeks later users reported the assistant had gotten dumber, because the model stopped choosing it.
- Now the crux, why it is more dangerous. Renaming a field makes callers fail loudly and someone finds you within five minutes. Editing a description raises nothing: unit tests pass, the server reports zero errors, logs are clean, and selection accuracy quietly drops a few points, surfacing weeks later as an undiagnosable 'it got worse'. Loud failures are far easier than silent degradation, so description changes need the stronger gate.
- Name the gate: an eval set with all three case categories, run on every description change, blocking the merge when selection accuracy drops. That is also why the eval must run fast from one command.
- Add the compatibility techniques: new fields must be optional, since old clients will not send them; to change semantics, introduce a new tool name rather than mutating in place, mark the old one deprecated with the replacement named in its description, and remove it only after a grace period, because you cannot know how many prompts hardcode that name.
- Likely follow-ups: how does the protocol version itself? MCP uses YYYY-MM-DD marking the last breaking change, backwards-compatible updates do not bump it, and deprecated features stay for at least twelve months before removal. And can you fix metadata after publishing to the registry? No: versions are unique and immutable once published, a typo costs a new version, and range-looking version strings are rejected outright.
答题要点
- 常规四类:删除或重命名字段、改字段类型、改变现有行为语义、新增必填字段
- 第五类是 MCP 特有的:改工具描述,因为描述是模型选工具的唯一依据
- 它更危险是因为不报错:测试全绿、日志干净,只有选中率悄悄下滑,几周后才浮上来
- 闸门是评估集;加字段要可选,改语义要换新工具名并给旧的一段弃用期
Key points
- The four usual categories: removing or renaming fields, changing types, altering semantics, adding required fields
- The MCP-specific fifth is editing a tool description, since the description is the model's only selection signal
- It is more dangerous because nothing fails: tests pass and logs are clean while selection accuracy silently drops
- The gate is the eval set; new fields must be optional, and semantic changes need a new tool name plus a deprecation window
线上有人反馈某个 MCP 工具「总是调不对」。你按什么顺序排查?A user reports that one of your MCP tools is 'always getting it wrong' in production. In what order do you investigate?
国内高频海外高频进阶#observability#debugging分析过程 · 先想清楚再作答
- 这题考的是排查的**顺序**,不是知识点的多少。上来就贴日志和堆栈的人会被追问「你怎么知道问题在服务端」。
- 第零步是把「调不对」翻译成三种互斥的现象,这一步不做后面全是猜:一是**没被调**(模型压根没选这个工具);二是**调了但参数错**;三是**调了参数也对,但结果不对**。问一句「那次它是没动,还是动了但做错了」,或者直接去日志里看有没有这条调用记录,就能分开。
- 对应三条不同的路。没被调,问题在**描述**:去跑评估集,看正例还是边界掉了;正例掉说明描述写糊,边界掉说明缺了区分相似工具的那句话。参数错,问题在 **schema**:看字段名是不是有歧义、描述里有没有写清格式、必填项是不是标对了;这类问题的信号是错误率里工具执行错误持续偏高——那通常不是模型笨,是 schema 没说清。结果不对才是代码问题,这时候才轮到单元测试和日志。
- 指标层面的顺序也说一下:**先看错误率,再看选中率**。错误率正常但用户说不好用,八成是选不中;错误率飙了才去看代码和上游。耗时看 P95 不看平均值,远程服务端上一个工具从 200 毫秒退化到 8 秒,平均值可能只动一点点。
- 还有两条容易被忽略但很常见的原因,要主动提。一是**聚合冲突**:客户端连了多个服务端,两个工具重名,模型选中的是另一个服务端的那个——这时候「你的工具」根本没被调,查你的服务端永远查不出来。二是**版本或缓存**:列表结果带 ttlMs 缓存提示,客户端可能拿着旧的工具清单;工具清单变了要靠 listChanged 通知才会重新拉。
- 结论:这条链上有四个环节——描述、schema、聚合与缓存、实现。**按模型看得见的顺序从前往后查**,因为越靠前的环节越不产生错误日志,也就越容易被跳过。
- 可预期的追问:怎么留证据?每次调用记一条结构化日志,字段里要有工具名、参数摘要与字段名、是否 isError、耗时、以及这次调用有没有经过人工确认;最后那一栏是事后区分「用户授意」和「模型自作主张」的唯一依据。
How to reason about it · think before answering
- This tests ordering, not breadth. Anyone who opens with logs and stack traces gets asked how they know the problem is server-side at all.
- Step zero is translating 'getting it wrong' into three mutually exclusive symptoms, without which everything after is guesswork: it was never called, it was called with wrong arguments, or it was called correctly and returned the wrong thing. Asking whether it did nothing or did the wrong thing, or simply checking whether a call was logged, separates them.
- Each symptom has its own path. Never called means the description is at fault: run the eval set and see whether happy paths or edges dropped, since happy-path drops mean a vague description and edge drops mean the sentence distinguishing similar tools is missing. Wrong arguments means the schema is at fault: ambiguous field names, unstated formats, wrong required markers. The signal is a persistently high tool-execution error rate, which usually means the schema is unclear rather than the model being dumb. Only a wrong result is a code problem, and only then do unit tests and logs matter.
- State the metric ordering too: error rate first, selection accuracy second. A normal error rate with unhappy users almost always means the tool is not being chosen; a spiking error rate sends you to the code and upstream. Watch P95, not the mean, because a remote tool degrading from 200 milliseconds to 8 seconds barely moves an average.
- Volunteer two commonly missed causes. Aggregation collisions: the client is connected to several servers, two tools share a name, and the model picked the other one, so your server was never called and investigating it will never find anything. And version or caching: list results carry ttlMs cache hints, so the client may hold a stale tool list, and refresh depends on a listChanged notification.
- Conclusion: the chain has four links, description, schema, aggregation and caching, and implementation. Walk it in the order the model sees it, because the earliest links produce no error logs and are therefore the ones people skip.
- Likely follow-up: how do you keep evidence? Emit one structured log per call with tool name, an argument digest plus field names, the isError flag, duration, and whether a human confirmed the call, that last column being the only way to distinguish user intent from the model acting on its own.
答题要点
- 先把「调不对」分成没被调、参数错、结果错三种互斥现象,再决定查哪里
- 没被调查描述并跑评估集;参数错查 schema;结果错才轮到代码与日志
- 指标顺序是先错误率再选中率;耗时看 P95 不看平均值
- 别漏掉聚合重名(选中的是别的服务端的同名工具)和工具清单缓存这两类原因
Key points
- First split 'getting it wrong' into never called, wrong arguments, or wrong result; the split decides where to look
- Never called points at the description and the eval set; wrong arguments at the schema; only a wrong result at the code
- Check error rate before selection accuracy, and read P95 rather than the mean
- Do not miss aggregation collisions, where another server's same-named tool was chosen, or a stale cached tool list
评论
登录后即可参与讨论
还没有评论,来说第一句。