AI 电子伴侣
创建时间: 2026-08-01最后更新: 2026-08-04

1. 选择框架前先选择流程

手写最小 RAG 以后,我们已经知道检索和生成分别做什么。接下来使用 LangChain 和 LangGraph,重点不再是减少几行代码,而是选择合适的执行方式。

公司制度问答通常希望每次都先检索,调用次数和延迟容易控制,适合 2-Step RAG。研究助手面对普通闲聊时可能不需要查资料,遇到事实问题时又要在多个知识源之间选择,适合 Agentic RAG。对权限、质量和重试有明确要求的系统,则更适合用 LangGraph 把步骤固定下来。

这三种方式没有高低之分。流程越自主,灵活性越高,测试空间和故障路径也会随之增加。

2. 使用当前依赖

本章按照当前 LangChain JavaScript 文档使用 createAgent(),向量存储使用 @langchain/classic 中的 MemoryVectorStore,文本切分使用独立的 @langchain/textsplitters 包。LangGraph 使用 StateSchemaGraphNodeStateGraph

terminal
1
yarn add langchain @langchain/core @langchain/classic \
2
@langchain/openai @langchain/textsplitters \
3
@langchain/langgraph zod

网上仍然能看到 createReactAgent()、旧版 chain helper 和 Annotation.Root() 等示例。它们可能对应旧版本或不同层级的 API,复制前要先核对当前官方文档和项目锁定版本。

LangChain 当前的 Retrieval 文档 将 2-Step、Agentic 和 Hybrid 作为不同 RAG 架构;LangGraph 当前 Graph API 使用 StateSchema 定义图状态。

3. 准备共享知识库

三种架构可以共用同一个 Retriever。我们先准备一份内存知识库:

knowledge-base.ts
01
import { Document } from '@langchain/core/documents'
02
import { MemoryVectorStore } from '@langchain/classic/vectorstores/memory'
03
import { OpenAIEmbeddings } from '@langchain/openai'
04
05
const embeddings = new OpenAIEmbeddings({
06
model: 'text-embedding-3-small',
07
})
08
09
export const vectorStore = await MemoryVectorStore.fromDocuments(
10
[
11
new Document({
12
id: 'leave-policy:v4:1',
13
pageContent:
14
'少于两天的年假,由直属负责人审批。',
15
metadata: {
16
source: '员工考勤制度',
17
version: 4,
18
topic: 'leave',
19
},
20
}),
21
new Document({
22
id: 'leave-policy:v4:2',
23
pageContent:
24
'连续两天及以上的年假,需要直属负责人和部门负责人共同审批。',
25
metadata: {
26
source: '员工考勤制度',
27
version: 4,
28
topic: 'leave',
29
},
30
}),
31
new Document({
32
id: 'expense-policy:v2:1',
33
pageContent:
34
'差旅报销应在费用发生后 30 天内提交。',
35
metadata: {
36
source: '差旅报销制度',
37
version: 2,
38
topic: 'expense',
39
},
40
}),
41
],
42
embeddings,
43
)
44
45
export const retriever = vectorStore.asRetriever({
46
k: 6,
47
searchType: 'mmr',
48
searchKwargs: {
49
fetchK: 20,
50
},
51
})

内存向量库只用于教学。替换为持久化服务时,Retriever 以上的流程仍然可以保留。

4. 2-Step RAG

2-Step RAG 的检索一定发生在生成之前。它执行一次 Retriever 和一次聊天模型,最适合作为第一版。

two-step-rag.ts
01
import type { Document } from '@langchain/core/documents'
02
import { ChatOpenAI } from '@langchain/openai'
03
import { retriever } from './knowledge-base'
04
05
const model = new ChatOpenAI({
06
model: 'gpt-4.1-mini',
07
temperature: 0,
08
})
09
10
function formatDocuments(documents: Document[]) {
11
return documents
12
.map((document, index) => {
13
return [
14
`[S${index + 1}]`,
15
`来源:${document.metadata.source}`,
16
document.pageContent,
17
].join('\n')
18
})
19
.join('\n\n')
20
}
21
22
export async function runTwoStepRag(question: string) {
23
const documents = await retriever.invoke(question)
24
const context = formatDocuments(documents)
25
26
const response = await model.invoke([
27
{
28
role: 'system',
29
content: `你是公司制度助手。
30
只能依据用户消息中提供的资料回答。
31
资料不足时说明无法确认。
32
回答事实时标注来源编号。`,
33
},
34
{
35
role: 'user',
36
content: `资料:
37
${context}
38
39
问题:
40
${question}`,
41
},
42
])
43
44
return {
45
answer: response.content,
46
documents,
47
}
48
}

这段代码没有把 Retriever 藏在 Agent 内部,因此容易测试和追踪。检索失败时查看 documents,回答失败时再检查 Prompt 和模型。

对于公司知识库、客服 FAQ 和固定领域问答,优先使用这种结构通常更稳。

5. Agentic RAG

Agentic RAG 把检索变成工具。模型可以根据对话判断要不要查、用什么查询词查,以及是否再次检索。

当前 LangChain v1 使用 tool() 定义工具,使用 createAgent() 创建 Agent:

agentic-rag.ts
01
import * as z from 'zod'
02
import { createAgent, tool } from 'langchain'
03
import { ChatOpenAI } from '@langchain/openai'
04
import { retriever } from './knowledge-base'
05
06
const searchPolicy = tool(
07
async ({ query }) => {
08
const documents = await retriever.invoke(query)
09
10
if (documents.length === 0) {
11
return '没有找到相关制度。'
12
}
13
14
return documents
15
.map((document, index) => {
16
return [
17
`[S${index + 1}]`,
18
`来源:${document.metadata.source}`,
19
document.pageContent,
20
].join('\n')
21
})
22
.join('\n\n')
23
},
24
{
25
name: 'search_company_policy',
26
description:
27
'查询公司的请假、考勤和报销制度。涉及公司规则时使用;普通闲聊不要调用。',
28
schema: z.object({
29
query: z
30
.string()
31
.min(2)
32
.describe('独立完整、适合检索制度的中文问题'),
33
}),
34
},
35
)
36
37
const agent = createAgent({
38
model: new ChatOpenAI({
39
model: 'gpt-4.1-mini',
40
temperature: 0,
41
}),
42
tools: [searchPolicy],
43
systemPrompt: `你是公司助手。
44
涉及公司制度时,必须先调用 search_company_policy。
45
只能根据工具返回的资料陈述制度。
46
工具没有找到资料时,不要自行编造。`,
47
})
48
49
const result = await agent.invoke({
50
messages: [
51
{
52
role: 'user',
53
content: '我想休两天年假,需要哪些人审批?',
54
},
55
],
56
})

工具描述很重要。描述过于宽泛,Agent 可能在普通聊天中频繁调用;描述过于狭窄,又会漏掉同义表达。

Agentic RAG 还要限制循环次数、工具权限和返回数据量。模型如果连续改写并检索五次,答案未必更好,延迟和成本却会明显增加。

6. 使用 LangGraph 固定质量流程

当流程中加入查询改写、检索判断、重试和降级后,把所有逻辑放进一个函数会越来越难读。LangGraph 适合把这些步骤明确成节点。

Drawing canvas

下面定义一个受控 RAG 状态:

rag-state.ts
01
import * as z from 'zod'
02
import { StateSchema } from '@langchain/langgraph'
03
04
export const RagState = new StateSchema({
05
question: z.string(),
06
retrievalQuery: z.string().default(''),
07
documents: z
08
.array(
09
z.object({
10
id: z.string(),
11
content: z.string(),
12
source: z.string(),
13
}),
14
)
15
.default(() => []),
16
retrievalPassed: z.boolean().default(false),
17
answer: z.string().default(''),
18
})

图状态只保存后续节点确实需要的数据。完整向量、数据库连接和模型客户端不适合塞进可持久化状态,可以通过模块依赖或 Runtime Context 传入。

接下来实现节点:

rag-nodes.ts
001
import * as z from 'zod'
002
import type { GraphNode } from '@langchain/langgraph'
003
import { ChatOpenAI } from '@langchain/openai'
004
import { RagState } from './rag-state'
005
import { retriever } from './knowledge-base'
006
007
const model = new ChatOpenAI({
008
model: 'gpt-4.1-mini',
009
temperature: 0,
010
})
011
012
const rewriteQuery: GraphNode<typeof RagState> = async (state) => {
013
const response = await model.invoke([
014
{
015
role: 'system',
016
content:
017
'把用户问题改写成独立、完整的制度检索问题。只输出改写结果。',
018
},
019
{
020
role: 'user',
021
content: state.question,
022
},
023
])
024
025
return {
026
retrievalQuery:
027
typeof response.content === 'string'
028
? response.content.trim()
029
: state.question,
030
}
031
}
032
033
const retrieve: GraphNode<typeof RagState> = async (state) => {
034
const documents = await retriever.invoke(state.retrievalQuery)
035
036
return {
037
documents: documents.map((document, index) => ({
038
id: document.id ?? `candidate-${index}`,
039
content: document.pageContent,
040
source: String(document.metadata.source ?? 'unknown'),
041
})),
042
}
043
}
044
045
const gradeSchema = z.object({
046
passed: z.boolean(),
047
reason: z.string(),
048
})
049
050
const gradeModel = model.withStructuredOutput(gradeSchema, {
051
name: 'grade_retrieval',
052
})
053
054
const gradeRetrieval: GraphNode<typeof RagState> = async (state) => {
055
if (state.documents.length === 0) {
056
return { retrievalPassed: false }
057
}
058
059
const grade = await gradeModel.invoke([
060
{
061
role: 'system',
062
content:
063
'判断候选资料是否包含回答用户问题所需的信息。不要补充资料之外的知识。',
064
},
065
{
066
role: 'user',
067
content: `问题:${state.question}
068
069
候选资料:
070
${state.documents.map((document) => document.content).join('\n\n')}`,
071
},
072
])
073
074
return {
075
retrievalPassed: grade.passed,
076
}
077
}
078
079
const generate: GraphNode<typeof RagState> = async (state) => {
080
const context = state.documents
081
.map((document, index) => {
082
return `[S${index + 1}] ${document.source}\n${document.content}`
083
})
084
.join('\n\n')
085
086
const response = await model.invoke([
087
{
088
role: 'system',
089
content:
090
'只根据提供的资料回答,并在事实后标注来源编号。',
091
},
092
{
093
role: 'user',
094
content: `资料:
095
${context}
096
097
问题:
098
${state.question}`,
099
},
100
])
101
102
return {
103
answer:
104
typeof response.content === 'string'
105
? response.content
106
: JSON.stringify(response.content),
107
}
108
}
109
110
const fallback: GraphNode<typeof RagState> = () => {
111
return {
112
answer: '现有制度资料不足以回答这个问题。',
113
}
114
}
115
116
export {
117
rewriteQuery,
118
retrieve,
119
gradeRetrieval,
120
generate,
121
fallback,
122
}

最后连接节点:

rag-graph.ts
01
import {
02
END,
03
START,
04
StateGraph,
05
} from '@langchain/langgraph'
06
import { RagState } from './rag-state'
07
import {
08
rewriteQuery,
09
retrieve,
10
gradeRetrieval,
11
generate,
12
fallback,
13
} from './rag-nodes'
14
15
export const ragGraph = new StateGraph(RagState)
16
.addNode('rewrite_query', rewriteQuery)
17
.addNode('retrieve', retrieve)
18
.addNode('grade_retrieval', gradeRetrieval)
19
.addNode('generate', generate)
20
.addNode('fallback', fallback)
21
.addEdge(START, 'rewrite_query')
22
.addEdge('rewrite_query', 'retrieve')
23
.addEdge('retrieve', 'grade_retrieval')
24
.addConditionalEdges('grade_retrieval', (state) => {
25
return state.retrievalPassed ? 'generate' : 'fallback'
26
})
27
.addEdge('generate', END)
28
.addEdge('fallback', END)
29
.compile()
30
31
const result = await ragGraph.invoke({
32
question: '我想休两天年假,需要哪些人审批?',
33
})

这个图仍然是固定流程,只是把质量判断显式放进状态和节点。后面可以继续增加“改写后重试一次”,但必须设置明确终止条件,避免图在低质量结果上无限循环。

7. 三种方式怎样选择

方式适合场景优点主要风险
2-Step RAG制度、FAQ、垂直知识库快、可预测、容易评测每次都会检索,灵活性较低
Agentic RAG多数据源研究助手Agent 能决定何时和怎样查路径不稳定,成本和延迟波动
LangGraph 工作流有质量门禁、重试和审批状态清楚,流程可控节点和状态设计成本更高

不要因为项目名称里有 Agent,就默认选择 Agentic RAG。很多 Agent 产品中的知识问答仍然适合固定检索;Agent 可以负责更高层的任务决策,检索内部保持确定性。

8. 常见实现问题

检索工具返回太多内容

Agent 工具一次返回几十个 chunk,会迅速占满上下文。工具应当返回经过筛选的结果和来源,完整调试数据放在追踪系统中。

查询改写改变原意

用户问“那超过两天呢”,改写有帮助;用户问“不要查公司制度,只说一般情况”,改写模型可能擅自补成制度问题。应保留原始问题、记录改写结果,并在评测集中加入否定和边界案例。

质量判断只看有没有结果

向量库几乎总能返回最相近的内容,documents.length > 0 不代表资料足够。需要结合分数、规则或评测模型判断相关性,并允许走无答案分支。

Agent 反复调用检索

工具描述冲突、结果格式不清或 Prompt 没有限制时,Agent 可能重复查询。应设置最大步骤数、记录工具轨迹,并让工具结果清楚表达来源与是否命中。

9. 面试中常见追问

为什么不全部使用 Agentic RAG?

固定知识问答的检索步骤明确,2-Step RAG 的延迟、成本和测试更可控。只有问题确实需要动态选择数据源或多轮探索时,Agentic RAG 的灵活性才值得额外复杂度。

LangGraph 对 RAG 有什么价值?

LangGraph 不负责提高向量相似度,它负责把查询改写、召回、质量判断、重试、生成和降级组织成可观察、可持久化的状态流程。

检索结果为空才降级吗?

不是。向量搜索通常不会真正为空,更重要的是候选是否相关、权限是否正确、分数是否达到经过校准的范围,以及资料是否覆盖问题所需事实。

10. 总结

LangChain 提供文档、Embedding、VectorStore、Retriever、Tool 和 Agent 抽象;LangGraph 负责把多步检索过程组织成明确状态和节点。

第一版知识库优先采用 2-Step RAG。需要动态选择检索工具时再使用 Agentic RAG;出现质量判断、重试和人工介入等稳定流程后,用 LangGraph 将它们显式建模。

下一篇不再增加框架能力,而是专门处理最棘手的问题:资料明明已经入库,为什么检索仍然不准,以及怎样用混合召回、查询改写和重排逐步改善结果。