检索增强生成(RAG)的痛点从来不在"有没有",而在"准不准"。用户问一个问题,向量数据库吐回来五篇文档,三篇相关两篇跑偏,大模型照样会被带沟里去。

corrective RAG(CRAG)的思路很直接:加一道质检工序。不是检索完直接扔给大模型,而是先让另一个模型审一遍——这些文档到底能不能回答问题?能就继续,不能就重写查询再搜一次。

打开网易新闻 查看精彩图片

这套 TypeScript 实现把流程拆成了五个状态节点。retrieve 节点负责从 Pinecone 拉取文档,默认取 top-5,支持按 namespace 过滤。这里有个细节:查询用的是 rewrittenQuestion,如果前一轮刚重写过,就用重写后的版本,否则用原始问题。

gradeDocuments 节点是 CRAG 的核心差异点。它给大模型塞了一个极简的 system prompt,只允许返回 good 或 bad 两个词。没有评分、没有置信度,就是二选一。这种硬截断的设计牺牲了粒度,但换来了稳定性——解析结果不需要容错处理,trim 一下直接用。

如果评级是 bad,流程进入 rewriteQuery。这里同样走一次大模型调用,system prompt 只有一句话:"Rewrite the user's query to improve vector retrieval." 重写后的查询会覆盖进状态,retrieve 节点下一轮就会用上。

状态管理用了 LangGraph 的 Annotation 机制。CRAGState 里除了继承 MessagesAnnotation 的对话历史,还挂了六个自定义字段:question(原始问题)、namespace(Pinecone 命名空间)、documents(检索结果)、retrievalGrade(质检评级)、rewrittenQuestion(重写后的问题)、answer(最终答案)、retryCount(重试计数器)。

retryCount 的存在说明这套实现支持循环。从代码结构看,rewriteQuery 之后应该有一条边指回 retrieve,形成"检索-质检-重写"的闭环。但截断的代码里没展示条件路由逻辑,具体最多重试几次、什么情况下放弃,需要看完整的边定义。

向量存储做了单例缓存。getVectorStore 用 module-level 的变量存 PineconeStore 实例,首次调用时初始化,后续直接复用。这种设计在 serverless 环境里要注意冷启动问题——如果实例被回收,下次调用会重新走一遍 fromExistingIndex。

模型配置硬编码了 gpt-4 和 temperature 0。零温度确保质检和重写的结果可复现,但代价是创造性为零。如果查询本身需要一定的语义泛化,完全确定性的输出可能反而限制重写效果。

整个流程的日志埋点很密集,每个节点进出都有 console.log,带 [CRAG] 前缀方便过滤。生产环境估计得换成结构化日志,但至少调试阶段能看清数据流。

这套代码没展示的是:answer 节点怎么实现、循环退出条件是什么、以及如果重试次数耗尽怎么处理。CRAG 论文里提到的一个细节是,即使文档评级为 good,也可以并行做一次 web 搜索作为补充——这部分显然没在这个实现里出现。

从工程角度看,这是一个最小可用的 CRAG 骨架。状态定义清晰,节点职责单一,但距离生产级还差异常处理、超时控制、成本监控这些运维层的东西。特别是 gradeDocuments 和 rewriteQuery 各走一次大模型调用,延迟和 token 消耗都得算进成本账。