跳到主要内容

Agentset 多轮检索 — agentic 自评管线与 deep research 深度研究

本章讲什么: 检索引擎(第 2 章)之上的「智能层」。普通 RAG 查一次就答;这里讲两条会自己拟查询、自己判断够不够、不够就再查的多轮管线,以及聊天路由如何在三档之间分发。

1. 三档聊天:一个路由分发

聊天入口 POST(apps/web/src/app/api/(internal-api)/chat/route.ts:57)按 body.mode 走三条路:

mode用什么一句话
(默认)queryVectorStore 查一次经典单轮 RAG:检索一次 → 拼 prompt → 流式答
agenticagenticPipeline多轮自评检索,边查边告诉前端进度
deepResearchDeepResearchPipeline多阶段深度研究,最后成一篇报告

一个共用的预处理:把多轮对话压成单条查询。 非 agentic 模式下,若有历史消息,先用 LLM 把「最近 10 条历史 + 当前问题」压缩(condense)成一个独立查询再去检索(chat/route.ts:86-107)——因为「它呢?」这种依赖上下文的问题,直接拿去向量检索是查不准的。

2. agentic 管线:查询→检索→自评→循环

它要解决的小问题: 用户问一个复杂/模糊的问题,一次检索往往捞不全。人类研究员会换几个角度搜、看够不够、不够再搜。agentic 管线就是把这套循环自动化。

核心是 agenticSearch(apps/web/src/lib/agentic/search.ts:12)。它的循环最多 maxEvals 轮(默认 2~3):

┌─────────────────────────── 每轮 (最多 maxEvals 轮) ───────────────────────────┐
│ ① generateQueries:LLM 看对话历史,生成一批新查询 │
│ (要求和已试过的查询不同 → 避免原地打转) │
│ ② 并行检索:对每个新查询跑 queryVectorStore(首轮还额外把原始问题当一个查询) │
│ (向量库支持 keyword 才按 query.type 选模式,否则降级) │
│ ③ 去重累积:命中的 chunk 按 id 去重塞进 chunks 字典 │
│ ④ evaluateQueries:LLM 看「对话 + 已捞到的全部 chunk」,判断 canAnswer? │
│ ⑤ 若 canAnswer 或 totalTokens ≥ tokenBudget → 跳出;否则下一轮 │
└────────────────────────────────────────────────────────────────────────────┘

2.1 生成查询

generateQueries(agentic/utils.ts:30)让 LLM 输出 JSON { queries: [{ type: "keyword"|"semantic", query }] },prompt 明确要求「最多 10 个、关键词查询只放 1~2 个关键词」(agentic/prompts.ts:1)。并且把已试过的查询列出来,要求新查询和它们不同(utils.ts:40-44)——这是防止循环原地踏步的关键。

2.2 并行检索 + 去重

每轮对所有新查询 Promise.all 并行检索(agentic/search.ts:48-75),命中 chunk 按 id 去重累积进 chunks 字典(search.ts:80-83)。首轮还会把用户原始问题本身也当成一个语义查询加进去(search.ts:52-59),保证不漏最直接的召回。

注意这里重排是写死的:rerank: { model: "cohere:rerank-v3.5", limit: 15 }(search.ts:67)。

2.3 自我评估:canAnswer

这是 agentic 的灵魂。evaluateQueries(agentic/utils.ts:62)把对话历史 + 当前已捞到的所有 chunk 喂给 LLM,让它回 { canAnswer: true|false }(agentic/prompts.ts:12)。

// 示意,改写自 agentic/search.ts:86 起
const { canAnswer, totalTokens: evalsTokens } =
await evaluateQueries(model, messages, Object.values(chunks));
totalTokens += evalsTokens;
if (canAnswer || totalTokens >= tokenBudget) break; // 够了,或烧完预算,就停

两个刹车: canAnswer === true(资料够了)或 totalTokens >= tokenBudget(默认 4096,烧太多了)。任一触发就停,避免无限检索。

2.4 生成答案 + 流式进度

agenticPipeline(agentic/index.ts:29)把上面的循环包进一个 UI 消息流:先 data-status 推「生成查询中 / 检索中(带具体查询词)/ 生成答案中」给前端(index.ts:61-89),检索完把去重后的 chunk 作为 data-agentset-sources 推给前端(index.ts:92-102),最后把 chunk 文本拼进 prompt 流式生成答案(index.ts:116-127)。还有个不流式的 generateAgenticResponse(index.ts:138)给 API 用。

3. deep research 管线:规划→搜索→评估→过滤→成文

它要解决的小问题: agentic 适合「答一个问题」;deep research 适合「就一个主题写一篇有结构、多来源的研究报告」。所以它阶段更多、角色更细。

DeepResearchPipeline.runResearch(apps/web/src/lib/deep-research/index.ts:469)的主流程:

主题 topic

▼ ① 规划:generateInitialQueries ── LLM 把主题拆成 3-5 个递进查询

▼ ② 初搜:performSearch ── 并行检索所有查询 → 去重
│ └─ 每条结果还会被 LLM「摘要」成与主题相关的精简内容

▼ ③ 迭代:conductIterativeResearch (最多 budget 轮)
│ └─ evaluateResearchCompleteness:LLM 看现有结果,缺什么就吐新查询
│ 新查询为空 → 提前结束;否则继续搜并入

▼ ④ 过滤:filterResults ── LLM 挑出真正相关的来源索引,截到 maxSources

▼ ⑤ 成文:generateResearchAnswer ── 流式生成报告(带 <think> 推理抽取)

3.1 多个 LLM「角色」

构造时传入 4 个模型角色(deep-research/index.ts:23):planning(规划/评估)、json(结构化抽取)、summary(摘要)、answer(成文)。实际在 chat 路由里目前 4 个角色用的是同一个模型(chat/route.ts:110-116),但管线设计上允许各阶段用不同模型(便宜模型干摘要、强模型写报告)。

3.2 关键阶段拆解

  • 规划(generateResearchQueries,index.ts:120): generateObjectresearchPlanSchema 让 LLM 吐一组查询字符串。
  • 检索 + 摘要(webSearch,index.ts:140):queryVectorStore 后,对每条结果再用 summary 模型摘要成与查询相关的内容(processSearchResultsWithSummarization,index.ts:183)——这是 deep research 比 agentic 多的一步,目的是压缩上下文。查询超 400 字符会被截断(index.ts:144)。
  • 完整性评估(evaluateResearchCompleteness,index.ts:290): 先让 planning 模型自由评估「还缺什么」,再用 json 模型把评估解析成结构化的新查询列表(两步:先自然语言推理,再结构化抽取)。返回空列表 = 研究完成。
  • 相关性过滤(filterResults,index.ts:353): 同样两步——planning 模型挑相关来源,json 模型解析成索引列表,再截到 maxSources(默认 5,config.ts:14)。
  • 成文(generateResearchAnswer,index.ts:520): streamText 流式生成,并用 extractReasoningMiddleware({ tagName: "think" }) 把模型的 <think> 推理段分离出来(index.ts:529-532)。

3.3 配置与预算

RESEARCH_CONFIG(deep-research/config.ts:14):budget: 2(迭代轮数)、maxQueries: 2(每轮最多查询数)、maxSources: 5(最终用几条来源)、maxTokens: 8192(报告长度上限)。SearchResults 类(deep-research/classes.ts:63)提供 add / dedup(按 id 去重)/ toString 等工具。

4. 两条管线对比

维度agenticdeep research
目标答一个问题就一个主题成一篇报告
阶段生成查询 → 检索 → 自评 → 循环规划 → 搜索+摘要 → 完整性评估 → 过滤 → 成文
停止条件canAnswer 或 token 预算迭代评估返回空查询 或 budget 用尽
是否摘要每条结果否(直接用原文)是(summary 模型先摘要)
LLM 角色单模型4 角色(planning/json/summary/answer)
代码lib/agentic/lib/deep-research/

5. 巧妙之处(可借鉴)

  • 「自评 canAnswer」做停止信号: 用 LLM 判断「资料够不够答」来决定是否继续检索(agentic/search.ts:86),比固定轮数灵活。
  • 要求新查询不同于旧查询: 防止多轮原地打转(agentic/utils.ts:40)。
  • 双重刹车: 语义信号(canAnswer)+ 硬预算(tokenBudget),保证一定收敛(agentic/search.ts:93)。
  • deep research 先摘要再入上下文: 用便宜模型把每条结果压成相关摘要,缓解长上下文(deep-research/index.ts:183)——注释里也点了「context length issue here!」(index.ts:297)。
  • 「自然语言评估 + 结构化解析」两步走: 先让强模型自由推理,再让小模型把推理结果抽成 JSON(index.ts:299-320),比一步直出 JSON 更稳。
  • 流式 + 推理抽取: extractReasoningMiddleware<think> 段从正文里分离(index.ts:529)。

6. 边界与局限

  • deep research 角色目前同模: 4 个角色实际传的是同一个模型(chat/route.ts:110),多角色是设计余量,未在默认路径发挥。
  • 大量 console.log: deep research 管线带很多彩色日志(index.ts 多处),偏 cookbook 风格,生产可观测性靠的是这些日志而非结构化埋点。
  • agentic 不裁剪 chunk: 代码里两处 // TODO: shrink chunks and only select relevant ones(agentic/index.ts:91:165),目前把去重后的全部 chunk 都塞进 prompt。
  • upstream usage 计量分散: 用量在路由层用 incrementUsage 加(chat/route.ts:33),引擎层多处 TODO。

7. 横向对比

相比「单轮 RAG」框架,Agentset 的这两条管线属于 agentic RAG(检索本身被包进一个会反思、会迭代的 agent 循环)。和同 shelf 里把检索做成 LLM「工具调用」的 agent 不同,这里检索循环是写死的固定流程(generate→search→evaluate),不是让 LLM 自由决定何时调工具——更可控、更可预测,代价是不如工具调用式灵活。deep research 则对标各家「Deep Research」产品的开源复刻:规划-搜索-评估-成文的多阶段流水。

8. 代码地图

主题文件路径符号名
聊天三档分发apps/web/src/app/api/(internal-api)/chat/route.tsPOST
agentic 检索循环apps/web/src/lib/agentic/search.tsagenticSearch
agentic 管线(流式)apps/web/src/lib/agentic/index.tsagenticPipeline / generateAgenticResponse
生成查询 / 自评apps/web/src/lib/agentic/utils.tsgenerateQueries / evaluateQueries
agentic 提示词apps/web/src/lib/agentic/prompts.tsGENERATE_QUERIES_PROMPT / EVALUATE_QUERIES_PROMPT
deep research 管线apps/web/src/lib/deep-research/index.tsDeepResearchPipeline
deep research 配置/提示apps/web/src/lib/deep-research/config.tsRESEARCH_CONFIG / PROMPTS
deep research 数据结构apps/web/src/lib/deep-research/classes.tsSearchResults