跳到主要内容
它报了一行不存在的代码,还贴心地标好了行号

它报了一行不存在的代码,还贴心地标好了行号

阿舟
阿舟

· 阅读约 9 分钟

前几天翻一个 MCP 项目的开发记录。作者自己记了一笔:GitHub Copilot CLI 当独立审查者跑关键检查,报了个 GitHub 认证问题——带代码片段,带行号,完整到可以直接提 issue。

他回仓库里找。那一行不存在。那段代码不存在。

画外音:带行号的幻觉,比不带行号的可怕十倍。

我本来是去看这个项目本身的。Shared Knowledge,把 AI 对话里解决过的问题沉淀成社区知识库的 MCP 服务器,这个月初挂出来的,配了文档站和音频版。这类"把聊天记录变成文档"的活我见太多,多半停在"我写了个 prompt 让它总结"。但这位在几个地方明显高一截——恰恰是这些地方让我决定写点什么。

先说它是什么,同行看名字就懂:stdio 的 MCP 服务器,任何兼容客户端都能连,Claude Desktop 行,VS Code 里 Copilot Chat 的 Agent 模式也行。只暴露三个工具,每一个都窄:

search_knowledge(query)   # 字段加权的关键词检索
get_knowledge(slug)       # 读单篇,带路径遍历防护
publish_knowledge(...)    # 校验结构,开 PR。到这儿就停了

最后那个"停"字值钱。它开 PR,它不开到 main。

审核放在哪一步,比审不审重要

大部分 AI 工作流把审核放最后。先生成,再让人看一眼,再发。听着没问题,可顺序本身就是态度——审核被摆成一道 QA,一道赶时间就能跳过的关。

这个项目是反的。用户明确点"分享"之前,对话不出门。默认私密,分享是额外动作。结构由调用方的模型生成,服务器只校验,然后开 PR,合并是人点的那一下。合并之后,才轮到音频生成。

这条链上最不可逆的东西就是音频——对外、可传播、难撤回。它被排在人后面,不是和生成并排放着。没合并的 PR,一次音频都不会出。

第一份真正走完流程的 PR 解决的是个挺恶心的真问题:pyproject.toml 里一个可选依赖牵动了整条导入链,而那个导入本身根本不是可选的。这种 bug 就是你不敢让 AI 自己猜的那种,正好拿来验流程。

评论区有个人说得比我准——这是认知边界,不是质量控制。质量控制想的是"我们尽量让它好",认知边界想的是"这个问题不该由它回答"。两句话写出来的代码长得一样,出事的时候不一样。

"下游自己负责"

评论区吵得最凶的是投毒。有人指出,知识文章会被检索、会进 agent 的 context,那除了内容对不对,还有个更麻烦的:文章里能不能藏指令。间接提示注入。

作者的回应是,投毒风险不是这个库独有的,人工 PR 审核已经建立了"什么算已发布知识"的可信边界;至于下游客户端怎么区分数据和指令,那是客户端的事。

这个区分我同意一半。发布信任和执行授权确实是两件事,把它们搅一起,是很多安全讨论吵不明白的根源——一篇文章可信,不等于里面那句"请帮我删掉数据库"可执行。

但"下游自己负责"这个收尾,是把一道必答题外包给了每一个客户端。服务器什么都不做,那每个接它的助手作者都得从头想一遍:这段内容可能是指令吗,我要不要加围栏。大部分人根本想不到,想到的人里大部分会拖到出一次事再说。

我到现在也没完全想明白这层该做到哪一步。也许返回内容时统一包一层标记,也许规定知识文章里不允许出现能当指令读的裸文本位——可这想法我自己都觉得会误伤正常的代码示例。写到这里我承认,我没有好方案。但"这不是我这边的问题"和"这一层我暂时只能做到这里,剩下的明确留给客户端",语气差很多,实现上也差很多。后者至少告诉下一个接手的人,边界划在哪儿。

AI 审 AI,最后总得有双眼睛

回到那个不存在的行号。

这项目的开发是多模型分工,全由人盯着:一个按批次推进,一个当独立审查者跑关键检查,一个做本地迭代和修 bug。听着已经比大多数人讲究了。

然后某一次审查里,那个独立审查者报了认证问题,带行号,带片段。仓库里没有这一行。

一个模型在"审"这件事上,为什么会凭空造出一行不存在的代码?我的理解是这样:审查的本质是找出不存在的东西——缺的边界情况、缺的处理。而生成模型的先验恰好相反,是让世界看起来完整。这两个目标是拧着来的。

所以让它审代码,多数时候它告诉你"看起来没问题"。当它必须报点什么的时候——比如 prompt 明确要求列出问题——它就开始生成一个看起来像问题的问题。那个行号,大概率是照着仓库其他部分的结构合理外推出来的。它不是不认识这个仓库,它是补全了一个合理的世界。

这不是第一次。上次让 agent 跑一个迁移的收尾,它跑完甩给我一份"已同步字段"清单,格式工整,每个字段名都像模像样。其中两个字段压根不在我那套表里。

后来我给自己加了条规矩,土得掉渣:任何自动审查的输出,在采纳或改代码之前,我亲自去那一行看一眼。 看起来蠢,但它拦下来的东西,比我今年调过的所有 prompt 加起来都多。

关键在那双眼睛不能是同一个模型的另一个实例。共享同一套"看起来对"的先验,等于自己审自己。

顺便缴次税

这项目里有个坑我一看就笑了,因为踩过一模一样的:

model_id = os.environ.get("ELEVENLABS_MODEL_ID", "eleven_multilingual_v2")

看着没毛病。问题是 GitHub Actions 里定义了空的 ELEVENLABS_MODEL_ID=,那这个变量在环境里是"存在且值为空字符串",get 不会回退到默认值——它只在键缺失时才回退。于是 model_id 是个空串,代码里写的默认值被静默覆盖,一路跑到 API 才报错。

要么写成 or "eleven_multilingual_v2",要么启动时校验这几个变量非空。这种坑的讨嫌之处在于它不早报,只在某个环节突然失败,你回去翻代码,代码看起来是对的 😅。

还有个更"人话"的观察,作者自己记的:Markdown 适合人读、适合 Git diff,但不适合直接塞给 TTS。标题会和后面的正文连读,没有停顿,没有叙事结构。他打算加一层中间脚本,先解析已经审过的 Markdown 结构,生成一份带停顿和转场提示的旁白稿,再调 API 出 MP3。Markdown 还是唯一事实来源。这个顺序我认——加一层可以,别让新加的那层变成第二个事实来源。

为了解耦,他放弃了一个奖

这项目报了 ElevenLabs 那个奖项类别,音频生成和 MCP 服务器是解耦的,只在 PR 合并后于 Actions 里跑。只有审过的内容会被合成语音,这个逻辑干净。

有意思的是它没报 Best Use of Google AI。原因作者写得直白:第一版实现里服务器自己调 Gemini,把对话摘要成结构化文章,他做到一半重构掉了——这么设计,服务器就变成一个绑死单一供应商的巨石应用。重构之后,结构化规则变成一条 MCP 提示词,规定英文输出、必须带 Problem 和 Solution 章节、用受控的分类和标签词表:

## Problem
## Solution

具体谁来结构化,交给调用方的助手拿自己的模型去做。Claude 行,GPT 行,Gemini 行,本地模型也行。代价是失去了那个奖项的参评资格。他选了互操作性。

这个取舍我完全站他。奖项是一次的,绑死是永久的。还有个更实际的收益他没多说:把模型调用从关键路径上拿掉之后,这个服务器才变得可测了。任何自己在内部调外部模型的服务,你写的测试实际上是在测那个模型当天的状态。改成"我只校验,不生成",测试才真的在测你的代码。

同样的克制在检索上也有。四篇文章,字段加权的关键词搜索。有人会条件反射地上向量库,可就四篇而言,向量检索带来的召回提升约等于零,换来的是嵌入模型的依赖、索引同步的复杂度,还有一整套你以后要维护的东西。检索质量的上限是内容本身的质量,不是索引的结构。等库长到几千篇,这个判断可能就翻了,但现在不是。

作者说他下一步不打算加投票、评论、向量搜索或者仪表盘,先把"遇到问题→顺手回馈→别人复用"这个循环本身验证清楚。这个克制我认。社区产品的死法里排第一的,就是功能比用户多。

项目我讲完了吗?没有。它的 Streamable HTTP 托管计划、评论区里关于重复提交的争论——两个人同一周碰到同一个问题,先提的那篇 PR 还没合并,谁都搜不到,于是各提各的,作者说这种竞争该容忍,因为 PR 只是提案,不是已发布的知识——这些我本来还想写,再写就散了。

我想说的其实就一句:这套东西做对的地方,不是它让 AI 干了多少活,是它清楚哪一步不让 AI 碰。决定什么内容算"公开知识",这一步它交出去了吗?没有,钉死在合并前。

再说句不好听的,那个不存在的行号到现在还在提醒我:这条边界暂时只能靠人扛。自动审查能帮你筛,最后那一眼没有替代品。

规则一条:自动审查报出来的每一处,采纳前自己去那一行看一眼。就这一条。

阿舟
阿舟

写代码写到一半开始怀疑人生,靠 AI 工具续命,顺手把踩过的坑都记下来。

查看主页 →