
AI 为什么总会偏离主线:Task Packet 与上下文治理
从权威事实源、当前唯一目标、旁支分流到机器身份绑定,解决长任务中的上下文丢失与主线漂移。含一份真实施工单自相矛盾导致范围蔓延的案例——AI 没跑偏,是我们给了它两个打架的指令。
「vibe coding 工程化」系列第三篇。第一篇讲为什么需要 Loop, 第二篇讲怎么用 Claude Code + Codex 搭起闭环。 这一篇解决你搭完之后必然会遇到的那个问题:明明有实施计划,AI 就是不按计划走。
先把结论放前面:
AI 偏离主线,通常不是模型能力不足,而是它同时面对太多相互竞争的目标, 却没有一个可机器核验的当前任务边界。
这句话里每个限定词都重要。不是「它不听话」,是「它面前有太多同样合理的事可做」。
一、AI 为什么容易偏离主线
上下文越长,不代表优先级越清晰。
这是最反直觉的一条。很多人的第一反应是「上下文不够,多喂点」——于是把需求、架构、历史讨论、上一轮的发现全塞进去。结果是:现在有一百件事同样「在场」,而在场不等于有序。
你开头那句「本轮只做 A」,到第 40 轮对话时,跟另外九十九件事在同一个池子里争夺注意力。它没有忘记那句话,它只是不再认为那句话最重要。
需求、架构、历史讨论和临时发现会互相竞争。
它们全都真实、全都相关、全都值得做。模型看到「这里有个明显的 bug」时,那个 bug 是具体的、当下的、可解决的;而「本轮唯一目标」是一句抽象的话。具体的东西天然更有引力。
模型倾向于解决眼前最具体的问题。
这不是缺陷,人也一样。区别在于人有一个「等会儿,这不是我今天该干的」的刹车,而这个刹车来自外部的、可核验的边界——你的排期、你的 PR 范围、你的同事会问「你怎么改这个」。
AI 没有这些外部约束。除非你给它。
聊天记录不是可靠的项目状态。
这条最要命。很多人把「我们讨论过」当成「这事定了」。但聊天记录是线性的、含糊的、没有版本的。三天前的一句「那就先这样吧」,你记得它是敷衍,模型记得它是决议。
二、常见但无效的解决办法
在讲对的做法之前,先把几条死路说清楚——因为它们都很直觉。
❌ 不断追加「请严格按照计划」。 这句话加十遍,权重不会变成十倍。它只是池子里的第一百零一件事。
❌ 把所有架构文档一次性塞进 Prompt。 你以为在提供上下文,实际在制造竞争。塞得越多,「本轮唯一目标」的相对权重越低。
❌ 每轮重新解释完整背景。 成本高,且每次解释都可能引入新的措辞差异——今天你说「候选序列」,明天说「重试队列」,模型会认真地把它们当成两个概念。
❌ 用一个越来越大的 control.md 复制项目状态。
这是最隐蔽的一条。你会得到一份跟真实项目状态永远差半拍的副本。而两份状态一旦不一致,模型会挑它更容易理解的那份——通常是你手写的那份,因为它更整齐。
❌ 发现一个问题就立即切换任务。 「反正都改到这儿了」——这句话是主线漂移的起点,不是终点。
这五条的共同点:它们都在往里加东西。 而问题不是「东西不够」,是「东西太多且没有优先级」。
三、Task Packet 是什么
Task Packet 是本轮的施工单。它不是文档,是边界。
一份最小的施工单只回答七个问题:
| 栏位 | 回答 |
|---|---|
| 当前唯一目标 | 这一轮只做哪一件事 |
| 权威文档引用 | 事实在哪儿(引用,不复制) |
| 架构不变量 | 这一轮不得破坏什么 |
| 允许修改范围 | 能动哪些路径(机器可解析) |
| 明确不做 | 哪些事这一轮禁止做 |
| 验收证据 | 什么命令、什么退出码算完成 |
| 停止和裁决条件 | 遇到什么必须停下来问人 |
注意最后一栏。大部分施工单模板都没有它,而它恰恰决定了 AI 在遇到岔路时是「自己选一个」还是「停下来」。
四、权威文档与 Packet 的关系
这是很多人搞错的地方:Packet 不是一份小型架构文档。
| 文档 | 回答 |
|---|---|
| 需求 | 为什么做 |
| 架构 | 最终要成为什么 |
| 详细方案 | 采用什么技术路径 |
| 实施计划 | 现在做到哪一步 |
| 台账 | 哪些事已经裁决 |
| Task Packet | 这一轮具体做什么 |
Packet 的每一行都应该是指针,不是副本:
| 权威输入 | 对齐位置 | 与本轮的关系 |
|---|---|---|
| 技术架构 | <path>#<section> | 不得破坏的架构约束 |
| 实施计划 | <path>#<item> | 当前实施项及前后依赖 |
| 台账记录 | <path>#<decision>| 已裁决项、遗留风险 |
为什么必须是引用而不是复制:复制会产生第二份真相。两份一旦不一致——而它们一定会不一致——你就有了一个新问题:哪份算数?而模型不会问你,它会自己挑一份。
五、「当前唯一目标」为什么重要
这一栏是整份施工单的核心。看两个写法:
❌ 错误的写法:
完成健康路由相关能力,并处理发现的问题。
这句话有什么问题?它每个词都对,但没有边界。
- 「健康路由相关能力」——相关到什么程度?改了路由,那监控算不算相关?
- 「处理发现的问题」——发现什么处理什么,等于授权无限扩张。
这不是目标,这是一张空白支票。
✅ 正确的写法:
在默认关闭的 flag 下,让异步重试消费已经持久化的候选序列,并保持旧路径字节级不变。
对比一下它多了什么:
| 作用 | |
|---|---|
| 「在默认关闭的 flag 下」 | 发布边界——不影响线上 |
| 「让异步重试消费」 | 动作——具体到能验证 |
| 「已经持久化的候选序列」 | 前置条件——不含「顺便把持久化也做了」 |
| 「保持旧路径字节级不变」 | 不变量——一句可被 diff 验证的话 |
好的唯一目标,读完之后你知道什么算做完,也知道什么算越界。
一个判断标准:如果这句话写不下一行,说明它不是一件事,该拆成两轮。
六、真实案例:施工单自己打架,AI 当然选择做
这是这篇最值钱的部分,而且比「AI 跑偏了」的故事更有意思——因为跑偏的不是 AI。
一份限流的施工单,两栏是这么写的:
当前唯一目标:给认证端点(登录家族 + 注册 + 口令重置核验)补挂跨副本生效的按 IP 限流,不误伤正常用户。
超出范围:注册、
/user/reset、/user/reset_by_code—— 待用户裁决。
看出来了吗?同一份施工单,目标里包含注册和重置,范围里又把它们标成「待裁决」。
Claude 的实际 diff 把三者一并改了。
然后 Codex 的审查意见(脱敏后):
MAJOR — Packet 与实际 diff 存在主线冲突:Packet 一面将 register、两个重置端点标为「超出范围、待用户裁决」,实际 diff 已将三者并入;另一面又在「当前唯一目标」中包含它们。该范围扩展涉及安全策略和优先级,需用户明确裁决并统一 Packet 后才能批准。
注意 Codex 说的是「统一 Packet 后才能批准」——它没有让 Claude 把代码改回去,而是指出施工单本身有问题。
这个案例说明了一件事,值得所有人记住:
主线漂移的源头,经常不在执行方,在施工单。
你给了它两个打架的指令,它选了更具体的那个(目标里明确写了「注册」),这不叫跑偏,这叫按指令执行。
所以每次觉得「AI 又不听话了」,先回头看施工单:你是不是同时说了「做」和「别做」?
七、旁支问题怎么处理
实施过程中一定会发现别的问题。发现旁支问题不等于要立刻修——这条不写死,Loop 会在第二天变成一个无穷无尽的 TODO 收集器。
四个去处,必须选一个:
| 分类 | 什么时候用 | 动作 |
|---|---|---|
IMPLEMENT_NOW | 不修就完不成当前目标,且仍在允许范围内 | 本轮修掉并测试 |
ACCEPTED_RISK | 风险明确、当前目标可交付、人已接受 | 记账,不阻塞 |
LATER | 有价值但不属于当前目标 | 进下一个 Packet,不切换主线 |
ESCALATE_USER | 改变架构语义、范围、优先级或发布风险 | 立即停,给证据和建议 |
用法上有个细节容易错:IMPLEMENT_NOW 的判据不是「这个问题严重吗」,是「不修能不能完成当前目标」。
一个严重但不阻塞当前目标的问题,正确去处是 LATER 或 ESCALATE_USER,不是「既然严重那就顺手修了」。严重程度决定它多快被处理,不决定它在这一轮被处理。
八、Heartbeat 怎么证明主线在推进
执行方每轮的进度汇报,只需要回答五个问题:
- 当前目标是什么(用业务语言,不是编号)
- 本轮增加了什么可验证的能力
- 是否偏离;如有,停在哪、需要什么裁决
- 还差什么
- 为什么下一步是当前最高优先级
第 2 条是核心:「可验证的能力」,不是「我改了三个文件」。改了三个文件不是进展,能跑通一条以前跑不通的路径才是。
还有一条反模式要挡掉:别只输出 A1、B3、Phase 2 这种内部编号。
编号是给机器看的。你一眼扫过去看到「A1 完成,进入 B3」——你根本不知道发生了什么。所以我的规则里有一条「编号字典」:
只有在编号表里定义过的编号,才允许出现在正文、Heartbeat、审查报告和证据包里。 同步给用户时必须先写人类可读名称,再写编号。
这条看着琐碎,实际拦住的是术语漂移:三天后 A1 变成了另一个意思,而没人发现。
九、怎么机器化地防止串任务
前面讲的都是「怎么写」。但能被机器执行的约束,才是约束。四条机器边界:
1. Packet、checkout、HEAD、Guard、Review Prompt 绑定在一起。 一个任务一套身份。不是「我记得这是那个任务」,是每个产物都带着任务标识。
2. 修改范围由 allowlist 机器检查。
施工单里「允许修改范围」那一栏要写成 path/**,别写自然语言——它要能被解析成 allowlist,然后跟真实 diff 比对。
这就是为什么我说**「允许修改范围」比「目标」更重要**:目标是一句自然语言,执行方可以有一百种理解;允许范围是一组路径,机器可以直接比对。
3. 多任务用独立 worktree。 两个任务共用一个工作树,diff 会串。这不是纪律问题,是物理问题。
4. 审查先核对主线,再看代码实现。 这条顺序很关键。上面那个限流案例里,Codex 的第一条 MAJOR 就是「Packet 与 diff 主线冲突」——它先问「你做的是不是这一轮该做的」,再问「你做得对不对」。
顺序反了,你会得到一份「代码写得很好、但不该在这轮写」的审查报告。
十、最后一条兜底信号
前面九条都是预防。但流程会自己变形,所以需要一条检测:
连续两轮只在改施工单、审查提纲、台账,没有代码或裁决增量——那不是流程严谨,那是空转。
出现这个信号,必须重新回答一个问题:这一步是否直接让当前唯一目标更接近完成?
如果答案是「不,但它让流程更完善了」——那就是漂移。而且是最难察觉的一种漂移,因为它看起来非常勤奋、非常有条理、每一轮都有产出。
它只是没有产出目标。
结论
上下文治理不等于给模型更多上下文。
真正重要的是三件事:
- 明确事实源——需求、架构、台账各管一段,Packet 只引用不复制。两份真相等于没有真相。
- 明确优先级——「当前唯一目标」只允许一件事。写不下就该拆。
- 明确当前边界——允许改哪些路径,机器说了算,不是执行方自己理解。
所以 Task Packet 的价值不是增加了一份文档。
它的价值是减少 AI 当前需要做出的选择。
它面前的事越少,跑偏的可能就越小。这跟模型强不强没关系——模型越强,能同时看见的合理选项越多,就越需要有人告诉它这一轮只做哪一件。
下一篇:机器门禁——为什么「请遵守」永远不够,以及哪些约束值得花代价变成一道会返回非零退出码的关卡。