pstack 拆解 12 · 理解(下):它为什么长成这样
第二件事,是它为什么长成这样。代码不带动机:你能读出它做了什么,读不出它为什么存在。而模型只会照抄它看到的形状,不会替你保留这个设计意图。why 就是去把动机找出来。
why
why 背后的想法是认识论的:代码不带动机。 你能读出代码做了什么,读不出它为什么存在,因为动机散落在提交、PR、ticket、文档和聊天里,而这些记录残缺、有偏、有时根本没有。所以它必须把"证据"和"推断"严格分开,而且把"没找到"也当成一种有效结果,而不是用一句听起来合理的猜测去填。
why 回答"它为什么长成这个样子"。前面几篇已经出现过它好几次,现在把它的机制讲全,因为它最能说明一个 skill 是怎么由原则、子步骤、参考文件和契约组装起来的。
核心判断:代码不带动机
它整个设计建立在一句话上:你能读出代码做了什么,读不出它为什么存在。
动机不在代码里,它散落在提交、PR、ticket、文档、聊天、事故记录里。这些记录都不完整,有偏,有时干脆没有。所以 why 的目标不是"给一个解释",是在这些残缺的记录里,把证据和推断分开,并且如实报告哪些没找到。
这决定了它的第一姿态:它不是一个写故事的人,是一个谨慎、克制、精确的调查员。它的输出里,一句带精确引用的原话,胜过一段听起来合理的总结。
第一步:把问题分类,并路由
它先判断你问的到底是哪一种:设计理由、权衡、动机边界、外部约束、死代码,还是一次宽泛的历史梳理。如果目标含糊("我们为什么这么做?"),它从上下文里做最合理的猜测,并且明说自己的猜测是什么,好让你能纠正。
它还会把你路由走,如果问题其实不是 why 能回答的:
- 你只是想找回自己最近的工作上下文,那是
recall。 - 你想用人类可读的方式总结工作,那是另一件事。
- 你想恢复某一段具体的会话,那是 session-pickup 这个 playbook。
第二步:先立代码锚点,再派调查员
这一步是最容易被跳过、也最关键的一步。在派出任何调查员之前,它必须先把调查锚定在具体的代码上。它要拿到:
- 相关文件的路径和行范围。
- 关键符号(函数名、类名、常量)。
- 一份初始提交列表:最近碰过这段代码的几次提交。
- 从 merge commit 的主题里抽出的 PR 号(形如
(#1234))。 - 提交信息或 PR 正文里提到的 ticket ID。
这一步它自己内联地做,用的就是普通的 git 和 gh 命令:
# 被调查那几行的最近改动
git blame -L <start>,<end> <file>
# 完整文件历史,带补丁,能穿过重命名
git log --follow -p -- <file>
# 最近 20 次改动,PR 号可见
git log --oneline -20 -- <file>
# 从提交信息里抽 PR 号
git log -1 --format=%B <commit>
# 拉 PR 的正文和讨论
gh pr view <number> --json title,body,author,createdAt,mergedAt,labels,closingIssuesReferences,comments,reviews为什么要先做这件事。因为调查员接下来要在七个不同的源里搜索,他们需要一组共同的坐标:文件、符号、提交哈希、PR 号、ticket ID。没有锚点,每个调查员都会各自从零开始,得出的引用对不上,合成的时候拼不起来。
第三步:一类证据一个调查员,全部并行
锚点立好之后,它进入调查。它先把你能接上的 MCP 列出来,把每一个映射到七个证据类别之一。
- 源码历史(git、gh):实现期的理由,常常藏在 review 讨论里。这一类永远派,是唯一保证可用的源。
- issue / ticket 追踪器(Linear、Jira、GitHub Issues):产品侧或业务侧的驱动因素,往往在工程之外。
- 长文档(Notion、Confluence、Google Docs):成文的设计理由,在它变成代码之前的地方。
- 实时团队聊天(Slack、Discord、Teams):从没进过文档的实时讨论。
- 基础设施可观测性(Datadog、Grafana):逼出这段代码的基础设施和运行时现实。
- 错误追踪(Sentry、Rollbar):驱动出防御性代码的那些具体异常。
- 产品分析仓库(Databricks、Snowflake、BigQuery):塑造了代码的产品和数据现实。
每个类别派一个调查员,一个调查员只管一个源,所有人在同一条消息里并发派出去。有一条硬规矩:不要让一个调查员同时覆盖多个源。 一个源一个调查员,是这套设计能并行又互不干扰的前提。
这里有一句很重要的话:目标是完整的覆盖图,不是最小的覆盖图。 空结果也要记录下来,不能跳过搜索。只有当你能给出一条写在纸面上的理由时才跳过,而且这类理由只有两种:
- 这个类别没有可用的 MCP。这要标记成一个缺口,不是一个选择。
- 这个源可证明地无关。这是个很高的门槛。比如"错误追踪跳过,因为目标是一个构建期脚本,没有运行时路径"。
如果只是"大概无关",不能跳过。
调查员的纪律
每个调查员拿到的 prompt 明确写着:收集证据,不要直接回答问题,推理交给合成器。它的纪律值得单独看,因为它们就是这套东西和"让模型编一个解释"的分界线。
- 引用,不要转述,当措辞本身重要的时候。引用要能让人几秒钟内跳过去自己确认。
- 先宽后深。 先撒一张大的网,免得漏掉相关上下文,再收窄。
- 记录你搜过什么,不只是找到了什么。 一个"没有"只有在读者知道搜过什么的时候才有用。查询要按原样记下来。
- 抵抗故事。 如果三条证据排得很整齐,第四条和它们矛盾,那第四条矛盾才是最有趣的发现,不要把它收起来。
- 考虑反事实。 在报告一个强发现之前,先问:如果我现在的理解是错的,我还会看到这个证据吗?证据会有什么不同?
- 绝不编造。 如果想把一个打了折的发现向上取整成一句确定的话,停下来,标成"partial"。合成器依赖你的输出是准的。
还有两条很容易犯的错,它专门点名:不要把机制当动机(把 limit = 50 改成 limit = 100 只说明改了什么,不说明为什么),不要从代码风格推断意图("作者选了函数式写法"是关于代码的观察,不是关于意图的证据)。
挑一个源看下去:Slack
七个源各有一份具体的手册。挑实时聊天这个最难搜的源,看它到底怎么搜。
Slack 是一把双刃剑。它经常是真正的决定发生的地方,尤其是那些不值得写文档的小改动;它也是最易失的源:线程会被删,频道会归档,搜索质量随时间退化。
搜索的次序是设计过的,从最省事的开始:
- 按作者搜。 搜 PR 作者在合并日期附近的发言。这一下把范围收得很窄,而且经常命中。
- 按关键词搜。 功能名、关键符号,连拼写错误和口语化的说法一起搜。
- 按 PR URL 搜。 Slack 里经常贴 PR 链接,直接搜
/pull/<number>。 - 按错误字符串搜。 如果这段代码处理某个具体错误,搜那个错误字符串,通常会把事故线程翻出来。
- 按频道收窄。
#eng-*工程讨论、#proj-*项目频道、#incident-*和#sev-*事故频道、以及这个代码所属团队自己的频道。 - 穿线程。 找到一条相关消息之后,拉整条线程。决定往往住在回复里。
什么叫这里的"好证据":一场明确辩论过权衡的讨论("我本来想用 A,但 B 更好,因为……");一条描述这段代码所防的那个 bug 的事故频道消息;评审提问和作者或负责人的权威回答;一次指向某场会议的引用;一位产品经理或面向客户的工程师解释某个客户需求的消息。
它同时列了坑:老消息可能因为保留策略已经没了,这时要标出"留存悬崖";大量决定发生在搜不到的 DM 里,你会漏掉,这是一个已知限制;Slack 很随意,一句"哈哈哈就这么干吧"不是决定,哪怕它出现在提交之前;单条消息脱离线程后意思会变,所以一定要拉线程;如果 MCP 没鉴权,停下来报告缺口,不要编造发现。
一个横切角度:事故
除了七个源,还有一份横切的手册,叫 incident-postmortem。它不是第八个源,是一个切入角度。
判断依据是代码看起来防不防。如果目标里有 null 检查、重试、超时、限流、feature flag、出口守卫、OOM 处理,那这段代码很可能有事故背景,于是要在每个源里专门找事故痕迹:Notion 里搜提到目标文件或错误串的事后复盘;Linear 里找标了 incident、sev-*、postmortem-action-item 的 ticket;Slack 里搜代码加入日期附近的 #sev-* 频道;git 里搜"fix for incident"这类提交;Datadog 里搜正式事故记录;Sentry 里找首次出现时间对齐 PR 上线日期的 issue。
它最想看到的是一种多源互证:一个 Datadog 事故 ID 出现在一个 Linear ticket 里,那个 ticket 出现在一篇 Notion 复盘里,复盘出现在一条 Slack 线程里,线程链到目标 PR,而 Databricks 里那个错误事件的数量在修复上线后下降。链条一旦合上,证据就很强。反过来,代码不像防御性问题,就跳过这一份。
第四步:合成,以及一套认识论
调查员只交证据,不交结论。合成由一个单独的合成器完成,它要做的事里最重要的一件是给每一条结论标定置信度。这套认识论是这个 skill 真正的骨。
它把每一条陈述放进五个档之一。
- Direct。 有一段明确的文字直接回答了问题。比如 PR 描述里写着"这个修复解决了用户超过一千条就无法翻页的问题"。措辞是确定的,必须带引用。
- Supported。 没有哪一条明说,但多条间接证据收敛到同一个结论。比如 PR 标题说"改进性能",ticket 打了 perf 标签,周围的提交都在同一条热路径上。措辞是"证据指向",要列出那几条证据。
- Inferred。 上下文里读得通,但没有任何东西明确支持它。读者要明白这是你的解释,不是记录里的事实。措辞要 hedge:"appears to""likely""suggests""one reading is"。
- Speculative。 一个说得通的假设,但证据很薄,别的解释同样成立。要明确标成猜测。
- Unknown。 你找了,没找到。这是一个有效且重要的结果。
档位决定了陈述进哪个输出小节,以及用什么措辞。配套的还有一份措辞清单:哪些词自带确定性("because""the reason is"),只能给 Direct 或 Supported 用;哪些词表示 hedge("appears to""likely"),给推断用;以及要避开的词("obviously""clearly""just""I think")。
它还专门警告两种认知陷阱。
一个是不要事后合理化。今天看起来"有道理"的代码,可能是为一个已经不成立、或者当时就错的理由写的。不要假设作者做了"对"的事然后倒推去证明;不要假设代码库里一致的模式是有意为之,它可能是复制粘贴;更不要把"证据缺失"变成"缺失的证据"("没人提过安全问题,所以安全不是问题"这个推理是错的)。
另一个是谄媚陷阱。用户经常把一个假设嵌在问题里:"我们为什么这么做,我猜是为了性能?"。why 不会直接确认它,而是把它当作候选之一,独立去查证据。证据支持就带引用说支持,不支持就说不支持,并给出证据实际支持的东西。用户的猜测是一次调查的提示,不是一个等待盖章的结论。
还有一条处理矛盾的规矩:两个源冲突时(PR 说一套,ticket 说一套),两个都摆出来,不要挑那个更顺的叙事。
以及最后一条,也是最有价值的:证据缺失时要如实说,而且要说具体。 一句"我们不确定"没用。有用的写法是:我们想在 ticket 追踪器里搜 A、B、C,翻了 2023 年以来碰过这个文件的 6 个 PR,还在仓库里 grep 了那个阈值的字面量,没有任何一处给出理由。这样你知道答案不在显眼的地方,需要去问一个人(原作者、产品负责人),或者判断这个问题不值得继续追。
它交给你的东西长什么样
输出是固定结构:问题、涉及的代码、我们找到了什么、我们能合理推断什么、互相竞争的假设、我们不知道什么、查过哪些源、以及一段置信度总结。
这里有两个细节值得记住。第一,"我们能合理推断什么"这一节要把推断链露出来:"基于 A 和 B,很可能 C,因为 D"。第二,提交之前合成器要过一遍校准检查,其中一个问题是:如果"我们不知道什么"这一节是空的,那就可疑。 历史调查几乎总有缺口,空的那种要么是证据异常完整,要么是有什么被扫到地毯下面了。
如果这个 why 是一次改代码的前置,它还会把梳理出来的东西转成一组约束:要保留什么、要改变什么、要避免什么、有什么风险。 这一组直接喂给后面的规划。
一个失败模式
它点名最常见的失败模式是 recency bias,假设最近一次提交就是权威。代码现在的形状,常常是很多早期决定一层层叠出来的。要往回追,不要停在最近一次改动。
参考
原文都在 pstack 上游仓库(cursor/plugins):