DeepSeeker-Code源码导读01-agentNudges

读懂 agentNudges:给一个会犯傻的模型兜底,设计上难在哪

这篇讲什么

DeepSeeker-Code 是个自研的 coding agent。这个系列不聊它怎么用,只带你读源码------每篇拆一个核心模块,讲清楚设计思想在哪、技术难点在哪,让你翻开源码时一眼能看懂那些"为什么这么写"。

第一篇从 agentNudges.ts 开始。它是 agent 主循环的"防呆层",专门治模型犯傻。挑它打头阵,是因为它小而完整,能把 agent 子系统的几个核心设计思想一次讲透。源码都在 src/core/src/agent/ 下。

一、它治什么、什么时候冒出来:六个 nudge 的触发场景

agent 主循环(runAgent.ts)是个 while(true),一轮轮"模型推理 → 调工具 → 拿结果 → 再推理"。理想情况模型自己好好干活、干完收尾。现实是,DeepSeek 在没有强约束时会犯六类典型的傻,agentNudges.ts 给每类傻配了一个 nudge。

下面这张表是读这篇的钥匙 ------它说清每个 nudge 在什么场景 下、满足什么条件 会被触发、多久推一次。实际用的时候,你观察到的现象就对应这里:

nudge 什么场景下模型会犯这个傻 触发条件 频率上限
PLAN_FIRST 复杂任务闷头就莽,不先规划 首轮 + 首条 prompt 疑似非平凡任务(多文件/重构/架构) 仅首轮 1 次
REPEAT_RETRIEVAL 反复读同一个文件、反复跑同一个搜索 read 同一路径第 3 次 / grep 同一检索第 2 次 整个 run 最多 3 次
TOOL_DIGEST 刚拿到工具结果,几乎不写总结就收尾 距上次工具 ≤ 2 轮 + 总结 < 30 字 整个 run 最多 2 次
EARLY_FINAL 才跑两轮就乐观宣布"完成了" 有内容 + 轮次 ≤ 2 + 无完成声明 整个 run 最多 1 次
PHANTOM 返回完全空的内容(空包/崩溃) 回复内容完全为空 最多重试 2 次
NUDGE 长任务跑很多轮,忘了目标/不知该不该停 每隔 40 轮 周期性,每 40 轮

举几个你实际会碰到的场景:

  • 你让它"重构这个模块",它没规划直接动手------第一轮 就会被 PLAN_FIRST 推一下:"建议先调 enter_plan_mode 把方案想清楚"。
  • 你发现它对一个文件读到第三遍------REPEAT_RETRIEVAL 生效:"这文件你早看过了,别整文件重读"。但如果你刚让它改过这个文件,它重读核验是允许的(计数被清零,不会被拦)。
  • 它跑完一条命令、拿到输出,却只回了一句"好的"就想结束------TOOL_DIGEST 把它拦住,让它基于命令结果继续。
  • 任务才两轮它就说"完成了"------EARLY_FINAL 推它再自检一遍每个子目标到底落地没有。
  • 一个长任务跑到第 41 轮,NUDGE 周期性出现:"你已跑约 41 轮,自评一下是不是该收尾了"。

注意表里每个 nudge 都有"频率上限"------nudge 不是无穷尽的,推够了就停,剩下的交给后面的硬刹车(repeatBreaker)。这是个贯穿全局的原则:nudge 是尽力而为的引导,不是必须成功的强制。 模型要是铁了心要犯傻,推几次没用就放弃。

理解了这六个 nudge 各自什么时候用,再去看源码,就能把那些抽象的常量和判定条件,对应到具体的触发场景上。接下来讲它背后的三个设计决策------这三个决策,比那六类傻本身更值得读源码时注意。

二、三个设计决策:这个文件的灵魂

决策一:nudge 是"用完即弃"的,绝不落盘

打开 agentNudges.ts,你会看到每个 nudge 消息长这样:

ts 复制代码
export type NudgeMsg = { role: 'system'; content: string };
// 注入时:
return { role: 'system', content: `${NUDGE_FENCE}\n${NUDGE_TEXT(round)}` };

注意------它返回的这个消息,只是临时拼出来、附加到这一轮推理请求的末尾,用完就丢 。源码注释叫它 "ephemeral 尾部副本":不进 message 数组、不写进会话记录(transcript)、不进上下文压缩。

为什么这么费劲?因为 DeepSeek 有个隐式前缀缓存:只要发给它的上下文前缀和上一轮一模一样,命中缓存,费用降到约十分之一。而 nudge 是动态的(这轮要推、下轮可能不推),一旦它写进对话历史,前缀每轮都在变,缓存全废。

所以设计上宁可增加复杂度(搞一套"临时副本"机制),也要保证对话历史的前缀纹丝不动。这是整个 agent 子系统最硬的约束之一(源码里标为 P0-4)。读 agentNudges 时记住一句话:它所有的"推",都是为了不留痕。

决策二:用"有状态调度器工厂",而非模块级全局变量

你会注意到整个文件导出的不是一个对象,而是一个工厂函数

ts 复制代码
export const createNudgeScheduler = (opts) => {
  // 一堆跨轮计数状态(per-run)
  const readCounts = new Map<string, number>();
  let phantomRetries = 0;
  // ...
  return { pickNudge, interceptFinal, noteToolCall };
};

主循环每次 run 创建一个实例(runAgent.ts:87):

ts 复制代码
const nudges = createNudgeScheduler({ firstPrompt, planMode: !!options.planMode });

为什么不用模块顶层的全局变量存这些计数?因为一个进程里可能同时跑多个 agent------主 agent 和它派生的子 agent(subagent)。用全局变量,它们的计数会串味:主 agent 读过的文件,子 agent 也算进去。

工厂模式让每个 agent run 拥有独立的状态副本,互不干扰。这是 agent 这种"可嵌套、可并发"场景下,状态管理的标配写法。读这个文件,先认出这个工厂结构,后面那些状态变量就好理解了------它们都是 per-run 的。

决策三:文案、阈值、触发逻辑全集中在这一个文件

翻到文件顶部,一大片常量(节选):

ts 复制代码
const NUDGE_EVERY = 40;            // 每 N 轮注入一次周期自评
const PHANTOM_RETRY_MAX = 2;       // 空响应最多重试次数
const EARLY_FINAL_MAX = 1;         // 整个 run 最多推 1 次
const READ_REPEAT_THRESHOLD = 3;   // 同一路径第 3 次读取 → 推
// ...文案常量...
const PHANTOM_TEXT = "你的上一条回复没有任何内容......";

文案和阈值全堆在这里,是有意的。文件头注释一句话点明设计哲学:

控制流与提示词解耦------runAgent 主循环只保留薄调用,文案/阈值/触发/预算集中于此。

意思是:主循环只管"调一下 nudge 调度器",不关心推什么文案、用什么阈值。这些"提示词层面的细节"全收拢在 agentNudges.ts。好处是------你想调 nudge 的措辞或触发条件,只改这一个文件,绝不会动到主循环的控制流。

这个"控制流与提示词解耦"是整个 agent 子系统的设计基调。你会看到 streamInferencerepeatBreakertoolScheduling 都是从主循环抽出来的独立模块。读源码时,把每个模块当成"一个职责清晰的纯单元",主循环只是把它们串起来的薄调度层。

三、三个方法 ↔ 主循环的三个时机

工厂返回三个方法,分别在主循环的三个时机被调用。记住这张对照表,读源码就能在两个文件间来回跳:

方法 作用 主循环调用点
pickNudge(round) 推理前:这轮要不要推一下 runAgent.ts:174
interceptFinal(text, round) 收尾时:拦不拦 runAgent.ts:227
noteToolCall(round, toolCalls) 调了工具:更新计数、查重复 runAgent.ts:235

具体说:

  • 每轮推理 ,调 pickNudge 拿一个 nudge 消息,拼到这轮请求末尾(就是决策一说的 ephemeral 副本)。
  • 推理完,如果模型没调工具 (想收尾),调 interceptFinal------返回 true 就拦住让它继续,false 就放行真正结束。
  • 如果模型调了工具 (在干活),调 noteToolCall------一方面重置"空回复"计数(在干活就不是空转),一方面检测有没有重复检索。

三个方法对应主循环三个分支,分工干净。读 runAgent.ts 时,搜 nudges. 就能定位到这三处。

四、三个技术难点深挖

这是这篇的重头戏。这三个点,是我写这块代码时真正卡过、改过的地方。

难点一:怎么检测"重复"又不误杀正常的长任务

治"读十遍"的那个 nudge,难点不在检测,在不误杀

模型正常干活时,连续调很多次同名工具是家常便饭------连读 5 个文件、连改 3 个位置。你要是简单地"同名工具调多了就拦",正常长任务全被掐了。

我的解法是分对象、配阈值、看上下文 。看 noteToolCall 里对读取的计数:

ts 复制代码
const READ_REPEAT_THRESHOLD = 3;   // 同一路径第 3 次读取 → 推
const GREP_REPEAT_THRESHOLD = 2;   // 同一检索第 2 次即推

if (name === "read_file") {
  const p = normPath(args?.path);
  const n = (readCounts.get(p) ?? 0) + 1;
  readCounts.set(p, n);
  if (n >= READ_REPEAT_THRESHOLD && !nudgedReads.has(p) && repeatNudges < REPEAT_NUDGE_MAX) {
    nudgedReads.add(p);
    repeatNudges++;
    repeatRetrievalPending = { role: "system", content: `你已第 ${n} 次读取「${p}」......` };
  }
}

关键在计数的粒度------按"具体路径"计,不是按"工具名"计。读 5 个不同的文件,是 5 个路径各算 1 次,谁都没超阈值;只有同一个文件读到第 3 次,才算重复。这就把"正常的多步操作"和"病态的反复看"区分开了。

阈值也有讲究:读要 3 遍才推(前两次可能是先浏览后细读,正常),搜索只要 2 遍就推(同一个 grep 跑第二遍几乎一定浪费)。

但光这样还不够。还有个边界:改过的文件,应该允许重读核验。所以每次 edit/write/create 之后,把这个路径的计数清零:

ts 复制代码
if (name === "edit_file" || name === "write_file" || name === "create_file") {
  const p = normPath(args?.path);
  if (p) { editedPaths.add(p); readCounts.set(p, 0); nudgedReads.delete(p); }
}

改完读一遍确认,是合理的,不该被当成"重复"。这一行清零,是这套检测能不能用的关键------少了它,模型改完文件想核验一下就被拦,反而妨碍正常工作。

读源码启示:判断"重复"这类问题,核心不是"调了几次",而是"在重复什么、是不是有合理理由"。这套计数 + 清零的设计,就是在反复拿捏这个度。

难点二:怎么防止"防傻机制"自己造出新傻

这是最反直觉的一个坑。防空手收尾(TOOL_DIGEST)那个 nudge,源码注释里藏着一段血泪:

ts 复制代码
// ★ 必须限长(< TOOL_DIGEST_TEXT_MAX_LEN):只拦"空手收尾",
//   绝不能拦"实质总结"。否则总结轮被 continue,每轮正文各落一条 assistant 消息 →
//   出现"连续多个 final、中间无 user"的没有结尾症状。
if (lastToolCallRound > 0 && round - lastToolCallRound <= 2
    && text.length < 30 && !looksComplete(finalText)) {
  // ...注入 nudge,拦住让它继续
  return true;
}

最早的版本,这个 nudge 只看"刚执行完工具就想收尾",没限制总结长度。结果------模型每次好好写总结,都被判定成"空手收尾"给拦回去重来。于是出现极其诡异的症状:对话里连续好几个"完成了",中间没有任何实质内容,因为每轮总结都被掐了重跑。

修复的关键是那个 text.length < 30只拦"总结短于 30 字"的真·空手收尾,绝不拦有实质内容的总结。 而且最终选择用"长度"判定,而不是关键词------注释里也承认,靠"已完成/已修改"这种关键词判断模型是不是真完成了,太不可靠(模型会说"已简化完成""都已就位"这种漏网词)。

读源码启示:一个兜底机制,设不好会比没有更糟------它能把一种傻,变成另一种更隐蔽的傻。看到带"★"和长注释的限制条件,要特别注意,那通常是踩过坑才加上去的。

难点三:跨轮状态怎么管,又不让它无限膨胀

nudge 调度器是 per-run 有状态的,跑几十上百轮,那些 Map 和计数会不会越积越大、吃内存?

解法是预算上限 + 定长裁剪。每个 nudge 都有"整个 run 最多推几次"的预算:

ts 复制代码
const REPEAT_NUDGE_MAX = 3;   // 重复检索 nudge 整个 run 最多 3 次
const TOOL_DIGEST_MAX = 2;    // 空手收尾最多 2 次
const EARLY_FINAL_MAX = 1;    // 早收尾最多 1 次

推满预算就不再推------避免模型死活不听、nudge 还在无限堆积。这是个关键安全阀:nudge 是"尽力而为"的引导,不是"必须成功"的强制。 模型要是铁了心要犯傻,推几次没用就放弃,交给后面的硬刹车。

(定长裁剪在 repeatBreaker.ts 里:recentSignatures 超过 6 个就 shift 掉最早的,防长会话内存膨胀。)

读源码启示:有状态设计的边界控制,和它的核心逻辑一样重要。读这类调度器,除了看它"怎么触发",一定要看它"什么时候停"。

五、推荐的源码阅读顺序

把这篇当地图,去读 agentNudges.ts 时建议这么走:

  1. 先扫顶部常量区(阈值 + fence + 文案)。这里是"调参面板",所有触发条件和措辞都在这,先建立全局印象。
  2. createNudgeScheduler 工厂 。理解 per-run 状态有哪些(readCounts、各种预算计数)。
  3. 看三个方法pickNudge(优先级排队)、interceptFinal(收尾判定)、noteToolCall(工具计数 + 重复检测)。重点看每个判定条件背后的注释。
  4. 回到 runAgent.ts ,搜 nudges.,看这三处调用,理解触发时机。
  5. 最后看 repeatBreaker.ts,它是这套防傻体系的"硬刹车"补充。

六、关联:软推之外的硬刹车

nudge 是软的,靠提示引导,模型可以无视。所以还得有硬的------repeatBreaker.ts,直接熔断。两道检测:

  • 完整签名(工具名 + 参数)连续 3 轮一模一样 → 精确重复,立即停。
  • 轮询同一个后台任务连续 4 轮 → 死循环,立即停。

第二道有个和难点一相通的设计:它按"目标"(task_id)判等,而不是按"工具名"。源码注释说得很直白:按工具名判等"注定误杀长任务(连读 N 个文件、连改 N 处都是同名不同目标)"。这跟难点一里"按路径计而非按工具名计"是同一个思想。

nudge 负责软引导、repeatBreaker 负责硬兜底,两者互补,一起构成 agent 的鲁棒性防线。

最后

agentNudges.ts 这一个文件,麻雀虽小,却把 agent 子系统的几个核心设计思想都体现了:ephemeral 机制保前缀缓存、工厂模式做状态隔离、控制流与提示词解耦。读透它,再去看 streamInferencerepeatBreaker 这些同结构的模块,会顺畅很多。

下一篇,我带大家读主循环本身------runAgent.ts,看这个 while(true) 是怎么把推理、工具、压缩、兜底串成一条完整流水线的。

项目源码开源在 github.com/xnk/deepSee... ,文章里提到的文件都在 src/core/src/agent/ 下,欢迎对着源码读。觉得这个导读系列有点意思,点个 star 是对我最大的鼓励。

总结

  1. 定位agentNudges.ts 是主循环的"防呆层",治模型六类典型犯傻;
  2. 三个设计决策:nudge 用完即弃(保前缀缓存)、工厂模式做状态隔离(per-run 互不串味)、文案阈值全集中(控制流与提示词解耦);
  3. 三个方法 ↔ 主循环三时机pickNudge(推理前)/ interceptFinal(收尾时)/ noteToolCall(调工具后),对应 runAgent.ts:174/227/235
  4. 三个技术难点:按"对象"而非"工具名"检测重复(避免误杀长任务)、用长度而非关键词判定空手收尾(防"治过头"造新傻)、预算上限控制有状态膨胀;
  5. 软硬互补:nudge 软引导 + repeatBreaker 硬熔断,共同构成鲁棒性防线。
相关推荐
Anhty1 小时前
2026 实测 4 款 AI 变声器|QQ 聊天伪装声线,告别僵硬假声
人工智能·功能测试·ios·智能手机·安卓
甲维斯1 小时前
DeepSeek Pro正式版太难用了!这还对标Fable5?!
人工智能
甲维斯1 小时前
DeepSeek 官方Harness开源,一夜44.6K Star,缓存99%!
人工智能
suaizai_1 小时前
MCP 2026:AI世界的USB-C新升级
人工智能
浅安的邂逅1 小时前
免费AI生图模型 agnes-ai和智谱GLM-4V-Flash
人工智能·ai作画·ai编程·ai生图
新新学长搞科研1 小时前
【双一流高校主办 | EI快稳检索】第五届图像处理、目标检测与跟踪国际学术会议(IPODT 2026)
人工智能
Wang's Blog1 小时前
AI Agent白手起家68: 智能体优化——计划执行与反思模式实战
人工智能
刘海东刘海东1 小时前
类数据的结构型的赋予生命的加权逻辑方程结构图(简称结构图)
人工智能