系列目录(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。
但有一个前提:先有评测集。
没有评测集,你无法证明图比单次调用好。你只能感觉它“更工程化”。
两者的差别可以列成一张表:
| 维度 | 一次调用 | 图编排 |
|---|---|---|
| 延迟 | 一次往返 | 每个节点一次往返 |
| 成本 | 一份 token | N 份 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 收窄,否则会召回别人的素材。
六、如果明天就做,顺序是什么
- 先建 50 条评测样本。没有这个,后面所有对比都是感觉。
- 用 LangGraph 包住“检索 + 评估”两步,feature flag 控制,旧路径保留。
- RAG 只读素材卡,不碰简历、不碰岗位。
- 跑一周,对比指标。好就留,不好就关。
指标至少看四个:命中率、事实错误率、P95 延迟、单次成本。
其中最重要的是事实错误率。检索让回答“更有依据”是主观感受;事实错误率有没有下降,才是它能留下来的理由。
注意顺序:评测集在最前面,RAG 在最后面。
很多项目反过来:先接向量库,再想怎么证明它有用。
七、小结
“以后会加”是一句没有成本的话。
所以我在博客里单独开了一页路线图,把每个能力的触发条件写下来。到了条件就做,不到条件就不做。
对 LangGraph 和 RAG,我的判断是:它们是工具,不是阶段。一个任务需要分支和状态,就用图;需要从大量数据里取一小部分,就用检索。任务本身不需要,加进来只会增加成本和不确定性。
上一篇:给 AI 立规矩:预览、确认、留痕(待发布)
下一篇:教 AI 读真实页面:表单识别的五轮迭代(待发布)