跳到主要内容

数据截至 (上游 commit eb1f187f8722)

Agentset 多轮检索 — 工具调用式 agentic 搜索与自评检索循环

本章讲什么: 检索引擎(第 2 章)之上的「智能层」。本章讲两代多轮检索:现在 chat 主路径的工具调用式 agentic 搜索(模型自己决定何时搜、搜什么、要不要扩上下文),和仍服役于 hosting-search 的固定流程式自评循环(生成查询→检索→LLM 自评→不够再来)。曾经第三条 deep research 多阶段管线已从代码库移除。

1. 聊天路由:legacy 三档已归一成一档

聊天入口 POST(apps/web/src/app/api/(internal-api)/chat/route.ts:43)如今只有一条执行路径:agenticSearchPipeline(工具调用式 agentic 搜索)。schema 里 mode 虽然仍接受旧枚举值,但会被归一(chat/schema.ts:41-48 注释写明「legacy modes (normal/agentic/deepResearch) map to accurate」):除 fast 以外全部映射成 accurate

归一后的 mode行为
accurate(默认,含 legacy normal/agentic/deepResearch)语义检索取 topK(默认 30)、重排后留 rerankLimit(默认 10)
fast跳过重排,topK/keywordTopK 都压到 rerankLimit(chat/route.ts:81-90)

路由里两个值得记的预处理:

  • 消息裁剪: 历史 turn 里的工具结果与推理段不再喂给模型(反正它会重新搜),pruneMessages 把「最后一条用户消息之前」的 reasoning/toolCalls 都剪掉(chat/route.ts:57-69);续写 payload(尾部是 assistant/tool)则原样保留,因为 Responses API 回放需要配对的 reasoning。
  • 旧版「多轮压缩成单查询」的 condense 已删除: 工具调用式管线不需要预先 condense——模型自己看完整对话自己拟查询。

2. 主路径:agenticSearchPipeline——模型驱动检索的工具循环

它要解决的小问题: 固定流程式检索(§3)的「何时停、换什么角度搜」是代码写死的;工具调用式把决定权交给模型——它看完结果觉得不够就换个 query 再搜,觉得某段被截断就 expand 上下文。

agenticSearchPipeline(apps/web/src/lib/agentic-search/index.ts:50)是 AI SDK streamText 的薄包装:

streamText(model, messages, tools = { search, expand })
│ stopWhen: stepCountIs(MAX_STEPS) ← 硬顶:20 步(index.ts:25)
│ maxOutputTokens: 5000
│ smoothStream(word) ← 逐词平滑输出

模型每步自行决定:调 search(新查询) / 调 expand(扩邻块) / 直接作答

三个防御细节:

  • 检索配置走「带外」: vectorStore/embeddingModel/搜索参数不进工具入参,而是通过 AI SDK 的 experimental_context 传给工具(agentic-search/tools.ts:23-29AgenticToolContext,注释明说「so the model can't influence retrieval configuration」)。模型能改的只有 query 文本和检索模式。
  • 能力降级: 向量库不支持 keyword 检索时,search 工具自动把 keyword 降级成 semantic(tools.ts:63);不支持有序取邻块(ordered query)时,整个循环只开放 search、藏掉 expand(index.ts:84-86activeTools)。
  • 计量恰好一次: 每次向量库查询经 onQuery 计数;finishRun 用幂等标志保证 run 正常结束、中止、出错都只调一次 afterRun(index.ts:70-74,onAbort/onFinish 双挂)。

2.1 两个工具

工具入参行为
search(tools.ts:53)query + mode("semantic"/"keyword") + label(仅给 UI 展示,不影响检索)queryVectorStore;语义模式才重排,keyword 模式永不重排;结果经 formatChunk 投影
expand(tools.ts:100)documentId + sequence_number(都要求与 search 返回的原值一致)expandChunk 取同文档前后各 5 块共 10 块(tools.ts:114-118)

两件配套的事:双投影——同一条 chunk,给 UI 的是带 filename/metadata 的 FormattedChunk,给模型的是 formatChunkForModel 的精简投影(agentic-search/format-chunk.ts:40:73);内部字段(documentId/namespaceId/tenantId/legacy llamaindex 属性等)被 INTERNAL_METADATA_KEYS 滤掉不外泄(:25)。

2.2 系统提示词:两套 prompt 的合成

resolveSystemPrompt(agentic-search/prompts.ts:117)判定存进的 prompt 是不是「默认形状」(isKnownDefaultPrompt,:86——空串、或 App 历史上自动持久化过的几版默认文案,冻结清单在 KNOWN_DEFAULT_PROMPTS,:75):是就用调优过的 AGENTIC_SYSTEM_PROMPT(:3,含工具使用规则/查询指南/完整性契约/<citation ids> 引用契约);是用户自定义 prompt 就原文保留、再追加 PLATFORM_CONTRACT_PROMPT(:97)——平台规则永远生效,保证自定义 prompt 也不会教坏引用格式。

3. 旧路径仍活着:agenticSearch 自评循环(hosting-search 专用)

chat 已换代,但这条固定流程循环没有删——对外搜索 API hosting-search 还在用它(apps/web/src/app/api/(internal-api)/hosting-search/route.ts:80agenticSearch,apps/web/src/lib/agentic/search.ts:13)。它的循环最多 maxEvals 轮(默认 3,search.ts:17):

┌─────────────────────────── 每轮 (最多 maxEvals 轮) ───────────────────────────┐
│ ① generateQueries:LLM 看对话历史,生成一批新查询 │
│ ② 并行检索:对每个新查询跑 queryVectorStore │
│ ③ 去重累积:命中的 chunk 按 id 去重塞进 chunks 字典 │
│ ④ evaluateQueries:LLM 看「对话 + 已捞到的全部 chunk」,判断 canAnswer? │
│ ⑤ 若 canAnswer 或 totalTokens ≥ tokenBudget(默认 4096) → 跳出 │
└────────────────────────────────────────────────────────────────────────────┘
  • 生成查询(agentic/utils.ts:33):LLM 输出 { queries: [{ type, query }] },prompt 要求新查询与已试过的不同(agentic/prompts.ts:1)——防原地打转。
  • 自评(agentic/utils.ts:66):LLM 回 { canAnswer: true|false }(agentic/prompts.ts:12)。
  • 双刹车(agentic/search.ts:94):语义信号 canAnswer + 硬预算 tokenBudget,保证收敛。

4. 两代管线对比

维度工具调用式(agentic-search,chat 主路径)固定流程式(agentic/,hosting-search)
谁决定何时检索模型(逐步工具调用)代码写死的循环
停止条件模型不再调工具,或 20 步硬顶canAnswer 或 token 预算
检索配置带外 context,模型不可改代码常量(rerank limit 15)
上下文扩展expand 工具取邻块
引用契约系统提示词强制 <citation ids>无(hosting 场景直接返回 chunk)
代码lib/agentic-search/lib/agentic/

deep research(lib/deep-research/:规划→搜索→摘要→完整性评估→过滤→成文的多阶段管线)已从代码库整体移除——DeepResearchPipelineRESEARCH_CONFIGSearchResults 等文件在 HEAD 已不存在,chat schema 里的 deepResearch mode 也只是被归一成 accurate 的 legacy 值。

5. 巧妙之处(可借鉴)

  • 检索配置带外传: 模型拿到的工具入参只有「查什么、怎么查的意图标签」,库连接/参数走 experimental_context(tools.ts:23-29)——把「模型可影响面」收窄到查询文本本身。
  • 幂等收尾计量: finishRun 用布尔闸保证 abort/finish/error 三条路都只计一次(index.ts:70-74)。
  • 自定义 prompt 也锁引用契约: PLATFORM_CONTRACT_PROMPT 无条件追加(prompts.ts:117-122),存量客户的老 prompt(还教着旧式 [3] 引用)不会把渲染搞坏。
  • 默认 prompt 冻结清单: KNOWN_DEFAULT_PROMPTS 把历史上自动持久化的每一版默认文案列成常量(prompts.ts:75-81),让「用户其实没自定义过」的行继续吃到调优默认——迁移逻辑与提示词演进解耦。
  • 同一条 chunk 双投影: UI 看富字段、模型看精简投影,内部元数据白名单外滤(format-chunk.ts:25)。

6. 边界与局限

  • 20 步硬顶没有语义刹车: 工具调用式的停止条件是 stepCountIs(20)(index.ts:25、:90),不像旧循环有「资料够不够」的自评信号;预算控制只剩步数与 maxOutputTokens: 5000
  • legacy mode 全部静默归一: 客户端传 agentic/deepResearch 不报错,但拿到的都是 accurate 行为(chat/schema.ts:44-46)——依赖旧 mode 语义的集成方需要自己发现。
  • 自评循环只服务 hosting-search: agenticSearch 循环(lib/agentic/search.ts)在 chat 侧已无调用点,两套管线并存,修 bug 要改两处。
  • upstream usage 计量分散: chat 侧在 afterRunincrementUsage(取「至少 1 次查询」),hosting 侧在路由里 incrementSearchUsage

7. 横向对比

Agentset 的 chat 检索如今是工具调用式 agentic RAG(检索是 LLM 的工具,何时调、调几次由模型决定),与固定流程式(§3 的 generate→search→evaluate 死循环)相对。同 shelf 里多数「RAG chat」产品停在单轮检索;Agentset 这版把检索循环交还给模型、用带外 context + 步数硬顶 + 引用契约三道闸兜底,是「灵活性换可控性」之后又把可控性买回来的样本。

8. 代码地图

主题文件路径符号名
聊天入口(归一 mode)apps/web/src/app/api/(internal-api)/chat/route.tsPOST
mode 归一 schemaapps/web/src/app/api/(internal-api)/chat/schema.tschatSchema
工具调用管线apps/web/src/lib/agentic-search/index.tsagenticSearchPipeline
search/expand 工具apps/web/src/lib/agentic-search/tools.tssearchKnowledgeBase / expandChunkTool / agenticTools
chunk 双投影apps/web/src/lib/agentic-search/format-chunk.tsformatChunk / formatChunkForModel
系统提示词合成apps/web/src/lib/agentic-search/prompts.tsAGENTIC_SYSTEM_PROMPT / PLATFORM_CONTRACT_PROMPT / resolveSystemPrompt
自评检索循环(hosting-search 用)apps/web/src/lib/agentic/search.tsagenticSearch
生成查询 / 自评apps/web/src/lib/agentic/utils.tsgenerateQueries / evaluateQueries