系列目录(4 篇已发布)

第三季 · AI 功能 · 第 12 篇

LangGraph 与 RAG:什么时候该加回来

先用,再删;以及什么条件下值得重新装回来。

先说结论:现在的系统里没有 LangGraph,也没有 RAG。

不是不会,也不是没试过。LangGraph 写过一版,后来删了;RAG 一直没做,因为暂时不需要。

但这两个东西迟早会回来。这篇写清楚:当时为什么删,什么条件下该加回来,以及如果明天就做,最小方案长什么样。

一、LangGraph 的那次尝试

项目里最早的一批功能,是岗位评估。

它的 Spec 是我写的。里面有这么一句:

以 LangGraph.js 实现受控工作流:
JD 结构化解析 → 偏好对照 → 简历证据映射 → 缺口/风险分析
→ 优先级决策 → 投递准备建议。

任务清单里也有一条对应的记录:

T-004:定义 LangGraph 岗位评估状态与六个受控节点;
实现 JobEvaluationProvider 接口和确定性 MockJobEvaluationProvider。
验证:Mock 图返回通过 Zod 的六类结果;测试确保无网络请求。

也就是说,这版真的做出来了。六个节点,一条图,Mock Provider 跑通,结果过 Zod。

但最后上线的是另一个东西:一个 19 行的 provider,一次结构化调用。

二、为什么删

三个原因。

第一,六个阶段之间没有控制流。

解析 JD、对照偏好、映射证据、识别缺口、算优先级、给建议——这六步是顺序的,没有一步需要“如果失败就回到上一步”,也没有一步需要并行。

图编排的价值在于分支、回退、状态在节点之间传递。没有这些,图只是一条被拆开的直线。

第二,成本是六倍。

每个节点一次模型调用。延迟从几秒变成几十秒,token 从一份变成六份,失败面从“一次调用失败”变成“六个节点里任意一个失败”。

第三,调试变难。

一次调用出错,你看输入和输出。六个节点出错,你要先判断是哪个节点、它拿到了什么、上一步给的对不对。

这不是 AI 写错了。Spec 是我写的,是我在需求阶段就过度设计了。

删掉之后,六个阶段被压进一段 task 文本:

提取岗位要求,对照简历证据,识别缺口与风险,
计算 0-100 整数匹配分,给出投递优先级和投递前建议。

输出结构没变,还是那六类结果,还是 Zod 校验。变的是调用次数。

三、什么条件下把 LangGraph 加回来

三个条件,满足一个就值得重新考虑。

条件一:出现真正的分支或回退。

比如评估时发现“简历证据不足”,需要回到上一步重新取证据;或者不同岗位类型走不同的评估路径。

只要流程里出现“如果 A 则 B,否则 C”,图就有意义。

条件二:单次上下文装不下。

现在 JD 和简历各截五万字,一次能放下。如果以后要同时看岗位、简历、素材库、历史面试记录,一次放不下,就必须分阶段,每个阶段装不同的东西。

条件三:需要人工介入的中间点。

比如“先让用户确认岗位要求提取得对不对,再继续评估”。这种暂停、确认、继续的流程,用图表达比用代码堆状态机清楚。

如果要做,草图是这样的:

parse_jd → retrieve_evidence → score_and_advise
                │
                └─ 证据不足 → 回到 retrieve_evidence(最多一次)

每个节点独立 Zod,独立记录耗时和 token;整条图有总预算;保留原来的单次调用作为 fallback。

但有一个前提:先有评测集。

没有评测集,你无法证明图比单次调用好。你只能感觉它“更工程化”。

两者的差别可以列成一张表:

维度一次调用图编排
延迟一次往返每个节点一次往返
成本一份 tokenN 份 token
可观测输入与输出每个节点的输入与输出
失败恢复整次重试节点级重试
适用顺序执行、没有分支有分支、回退、人工介入

这张表里,只有最后一行是图的优势。前四行都是成本。

四、RAG 为什么现在没有

因为当前的事实来源是结构化的。

简历全文几万字,直接放进去,模型看到的是完整上下文。检索一上来,就有召回错误的风险:该看到的段落没被召回,模型就会基于不完整的信息回答。

在事实密集的场景里,检索错了比不检索更糟。

那什么时候需要检索?

不是简历,是那些“多到放不下、又只需要看一小部分”的东西:

  • 素材卡库。现在几十张,以后几百张,投一个岗位只需要其中三五张。
  • 岗位库。几千条岗位,要按语义找“和这个岗位相似的”。
  • 面试题库。积累到几百条之后,按岗位和技术方向找相关题目。

五、如果做 RAG,最小方案

不做“把简历切块检索”,先做“素材卡检索”。

存储:PostgreSQL 加 pgvector,一张 material_chunk 表,存素材变体的正文向量和来源 id。

切块:按变体切,不按固定长度切。一条实习经历本来就是一个语义单元,切碎反而丢上下文。

检索:混合检索。关键词(岗位名、技术栈)加向量,先各取十条,再去重排序。

使用:top-k 五条,拼进 prompt 时带来源 id;回答里必须能指回是哪张卡。

兜底:检索结果为空或分数过低时,退回“把全部素材卡摘要放进去”的旧路径。

开关:做成 feature flag。出问题能一键关掉。

指标:命中率(该出现的卡有没有出现)、事实错误率、延迟、token 成本。

存储大概是这样,用 pgvector:

create extension if not exists vector;

create table material_chunk (
  id         text primary key,
  user_id    text not null,
  card_id    text not null,
  variant_id text not null,
  content    text not null,
  embedding  vector(1536),
  updated_at timestamptz not null default now()
);

create index on material_chunk
  using ivfflat (embedding vector_cosine_ops);

检索的流程:

query
→ 先按 user_id、素材类型过滤
→ 向量检索 top 20
→ 按 card_id 去重
→ rerank 取 top 5
→ 拼进 prompt,每段带 variant_id

注意第一步的过滤。多用户系统里,向量检索必须先按 user_id 收窄,否则会召回别人的素材。

六、如果明天就做,顺序是什么

  1. 先建 50 条评测样本。没有这个,后面所有对比都是感觉。
  2. 用 LangGraph 包住“检索 + 评估”两步,feature flag 控制,旧路径保留。
  3. RAG 只读素材卡,不碰简历、不碰岗位。
  4. 跑一周,对比指标。好就留,不好就关。

指标至少看四个:命中率、事实错误率、P95 延迟、单次成本。

其中最重要的是事实错误率。检索让回答“更有依据”是主观感受;事实错误率有没有下降,才是它能留下来的理由。

注意顺序:评测集在最前面,RAG 在最后面。

很多项目反过来:先接向量库,再想怎么证明它有用。

七、小结

“以后会加”是一句没有成本的话。

所以我在博客里单独开了一页路线图,把每个能力的触发条件写下来。到了条件就做,不到条件就不做。

对 LangGraph 和 RAG,我的判断是:它们是工具,不是阶段。一个任务需要分支和状态,就用图;需要从大量数据里取一小部分,就用检索。任务本身不需要,加进来只会增加成本和不确定性。


上一篇:给 AI 立规矩:预览、确认、留痕(待发布)

下一篇:教 AI 读真实页面:表单识别的五轮迭代(待发布)