数据截至 (上游 commit 1f8f3e285bbc)
04 · 巧妙之处、边界、横向对比与代码地图
这章把前三章的精华抽出来当"可带走的技术",诚实列局限,做横向对比,最后给一张可直接 grep 的代码地图。
5. 巧妙之处(可借鉴的技术)
① 用「自递归 + 旋钮衰减」表达搜索树。 不引入任何 planner/state machine,deepResearch 调自己就把广度×深度的树展开了,breadth/2、depth-1 保证收敛。妙在"整棵树"的复杂行为压进一个约 120 行的函数(src/deep-research.ts:176-294)。可借鉴:能用递归表达的树形 agent,别急着上编排框架。
② 让 LLM 自己写「下钻方向」。 想搜 索词时顺带产出 researchGoal(src/deep-research.ts:66-70),这段目标就直接当成下一层的题目。等于把"下一步查什么"的规划外包给 LLM,代码只负责搬运。可借鉴:让模型在同一次调用里既产出动作、又产出"这个动作的意图/后续",省一次规划调用。
③ schema 即 prompt。 zod 的 .describe() 字段描述同时是校验规则和给模型的微提示(src/feedback.ts:18-24 等),一处声明两处生效。可借鉴:结构化输出场景,把"你要什么"写进 schema 描述而不是只写在 prompt 正文。
④ 中间件剥离 reasoning。 extractReasoningMiddleware({ tagName: 'think' })(src/ai/providers.ts:44)让业务代码对"推理模型会吐思考过程"这件事无感。可借鉴:用模型无关的中间件层吸收"某类模型的怪癖"。
⑤ token 截断的双重防死循环。 trimPrompt 里"截完还一样长就硬切"和"chunkSize 低于下限就硬切"(src/ai/providers.ts:81-94)两个兜底,是真跑过才会加的补丁。可借鉴:凡是"估算 + 递归收敛"的算法,都要为"估算失灵"准备一条硬退路。
⑥ 来源列表不过 LLM。 ## Sources 是代码手工拼的(src/deep-research.ts:144-146),绕开模型,保证引用链接不被幻觉篡改或丢失。可借鉴:能确定性生成的部分,别交给 LLM。
6. 边界与局限(诚实)
- 失败静默降级。 单支搜索/提炼出错就返回空并继续(
:275-285),最终报告可能悄悄缺一整支的发现,用户无从察觉。 - learnings 只在顶层去重。 递归下传时直接拼接(
:238),同一条要点会随深度复制进多个子 prompt,深度大时 token 浪费明显。去重仅发生在最外层Set(:290-293)。 - 无"够了就停"的判断。 停止条件纯粹是
depth耗尽,不看"信息是否已饱和"。depth 给大了会无谓地搜。 - 无引用溯源到句子。 报告尾部只有一个总
Sources列表,某条论断具体来自哪个 URL 无法对应。 - 进度上报不精确。 并发共享一个
progress对象就地改(:200-203),字段是"最近一次"快照,非逐支账本。 src/api.ts的/api/generate-report有 bug。 它return report(src/api.ts:84)却从不把报告写进 HTTP 响应(没有res.json/res.send),客户端拿不到结果——CLI 路径(run.ts)才是主力,这个 HTTP 端点像未完成品。- 上下文靠堆 learnings,非向量检索。 没有向量库/RAG,全靠把 learnings 文本拼进 prompt,规模上不去。
7. 横向对比
| 维度 | dzhng/deep-research | 典型"重"型 research agent |
|---|---|---|
| 编排 | 一个自递归函数 | 显式状态机 / 图 / planner |
| 停止 | depth 耗尽 | 信息饱和判断 / 反思循环 |
| 记忆 | learnings 文本拼接 | 向量库 + RAG 检索 |
| 搜索 | Firecrawl 一把梭(搜+抓正文) | 搜索 API + 独立抓取 + 清洗 |
| 定位 | <500 LoC 教学参考实现 | 生产级、可观测、可恢复 |
取舍很清楚:它用"极简 + 递归"换"可读、易改、易学",放弃了饱和判断、细粒度溯源、可恢复性这些生产特性。作为"deep research agent 最小骨架"的参考,它的价值正在"少"。
8. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| CLI 入口/问答流程 | src/run.ts | run、askQuestion |
| HTTP 入口 | src/api.ts | app(/api/research、/api/generate-report) |
| 澄清问题生成 | src/feedback.ts | generateFeedback |
| 递归引擎(心脏) | src/deep-research.ts | deepResearch |
| 搜索词 + researchGoal 生成 | src/deep-research.ts | generateSerpQueries |
| 结果提炼 learnings | src/deep-research.ts | processSerpResult |
| 长报告汇总 | src/deep-research.ts | writeFinalReport |
| 精确答案汇总 | src/deep-research.ts | writeFinalAnswer |
| 进度类型 | src/deep-research.ts | ResearchProgress、ResearchResult |
| 并发上限常量 | src/deep-research.ts | ConcurrencyLimit |
| 模型多提供商切换 | src/ai/providers.ts | getModel、o3MiniModel、deepSeekR1Model、customModel |
| token 截断 | src/ai/providers.ts | trimPrompt、MinChunkSize、encoder |
| 递归字符分割器 | src/ai/text-splitter.ts | RecursiveCharacterTextSplitter、mergeSplits、splitText |
| 统一系统人格 | src/prompt.ts | systemPrompt |
怎么用这张表
- 想改"树怎么长" → grep
deepResearch、newBreadth/newDepth。 - 想换/加模型 → grep
getModel,改src/ai/providers.ts的构造与优先级。 - 想调 prompt / 输出结构 → 找对应调用点的
generateObject+schema。 - 想改截断行为 → grep
trimPrompt、RecursiveCharacterTextSplitter。