0. 先说核心结论:LangChain 到底解决了什么问题
在一个真实 Agent(比如售后客服 Agent)里,你会同时遇到这些工程问题:
- 模型厂商 SDK 不统一(OpenAI / Anthropic / Gemini / 本地模型)
- Prompt 组织混乱(系统提示词、用户提示词、变量注入)
- 输出不稳定(有时 JSON,有时自然语言)
- 工具调用协议不统一(查订单、查质保、建工单)
- RAG 流程重复造轮子(加载、切分、向量化、检索)
- 多轮上下文难维护(用户分几轮补信息)
- 线上稳定性治理(超时、重试、降级)
- 追踪排障困难(为什么没走换货)
LangChain 的定位就是:把这些“重复底层工程”做成统一中间层。
你可以把它类比为 AI 应用里的 JDBC + 调用链框架 + 统一工具协议层。
1. 统一业务场景(贯穿全文)
用户第一句话:上周我买的一个耳机坏了
Agent 要完成:
- 识别意图(售后/其他)
- 抽取槽位(订单号、购买时间、故障描述、是否在保)
- 缺信息就追问(多轮)
- 检索并命中政策(7天退/15天换/质保修)
- 调用工具(查订单、建工单)
- 输出结构化结论给下游系统
2. 十个模块总览(先看全貌)
- 模型标准层(Model Abstraction)
- Prompt 工程层(Prompt Templates)
- 输出控制层(Structured Output)
- 工具调用层(Tools)
- RAG 数据接入层(Load/Split/Embed/Retrieve)
- 检索增强策略层(Re-rank / Hybrid / Query Rewrite)
- 链式编排层(LCEL / Runnable)
- 多轮上下文与记忆层(Message History / Memory)
- 可观测与评估层(Tracing / Eval)
- 运行治理层(Retry / Timeout / Fallback / Cache)
说明:并不是 10 点都必须写很多代码;有些是“能力层与工程策略”。
3. 模块展开(按“要解决什么 -> LangChain 怎么做 -> 代码对比”)
3.1 模型标准层(像 JDBC)
3.1.1 你自己直接写(不用 LangChain)
// 目的:对不同厂商模型做统一调用
// 问题:你要自己处理不同 SDK 的参数、返回结构、异常类型
async function callModelWithoutLangChain(
provider: 'openai' | 'anthropic',
prompt: string,
) {
if (provider === 'openai') {
// OpenAI SDK 的调用方式
const res = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
})
// OpenAI 返回结构
return res.choices[0]?.message?.content ?? ''
}
// Anthropic SDK 的调用方式
const res = await anthropic.messages.create({
model: 'claude-3-5-sonnet-latest',
max_tokens: 1024,
messages: [{ role: 'user', content: prompt }],
})
// Anthropic 返回结构不同,你又要单独处理
const first = res.content[0]
return first?.type === 'text' ? first.text : ''
}
3.1.2 用 LangChain
import { ChatOpenAI } from '@langchain/openai'
import { ChatAnthropic } from '@langchain/anthropic'
// 目的:把“厂商差异”收敛到初始化阶段
function createModel(provider: 'openai' | 'anthropic') {
if (provider === 'openai') {
return new ChatOpenAI({ model: 'gpt-4o-mini' })
}
return new ChatAnthropic({ model: 'claude-3-5-sonnet-latest' })
}
const llm = createModel(process.env.LLM_PROVIDER as 'openai' | 'anthropic')
// 统一调用方法:invoke
const result = await llm.invoke('用户说:上周买的耳机坏了,请识别意图')
结论:LangChain 把“多厂商 SDK 适配”变成可替换层,而不是业务层。
3.2 Prompt 工程层
不用 LangChain(字符串拼接,易散乱)
// 问题:提示词散落,变量注入靠手拼,容易漏字段/格式不一致
const prompt = `
你是售后助手。
规则:先识别意图,再抽取订单号/购买时间/故障描述。
用户输入:${userText}
`
用 LangChain(模板化管理)
import { ChatPromptTemplate } from '@langchain/core/prompts'
// 目的:把系统规则和变量输入显式化,便于复用/评审/版本管理
const classifyPrompt = ChatPromptTemplate.fromMessages([
['system', '你是售后助手。先识别意图,再抽取槽位。'],
['human', '用户输入:{input}'],
])
const formatted = await classifyPrompt.invoke({ input: '上周买的耳机坏了' })
3.3 输出控制层(结构化输出)
不用 LangChain(手动 JSON 解析 + 手动兜底)
const raw = await callModelWithoutLangChain('openai', prompt)
let data: any
try {
// 手动解析,遇到“半 JSON”时很脆弱
data = JSON.parse(raw)
} catch {
// 你还得手写重试或修复逻辑
throw new Error('模型输出不是合法 JSON')
}
用 LangChain(Schema 约束输出)
import { z } from 'zod'
// 目的:让模型输出契约化,减少后续流程判断混乱
const SlotSchema = z.object({
intent: z.enum(['after_sales', 'other']),
orderId: z.string().optional(),
buyDate: z.string().optional(),
issue: z.string().optional(),
inWarranty: z.boolean().optional(),
})
const structuredModel = llm.withStructuredOutput(SlotSchema)
const slots = await structuredModel.invoke('上周买的耳机坏了')
3.4 工具调用层(Tools)
不用 LangChain(你自己维护“工具协议”)
// 问题:调用哪个工具、参数是否合法、失败如何处理,全部手写
const tools = {
queryOrder: async (orderId: string) => omsApi.getOrder(orderId),
createTicket: async (payload: { orderId: string; policy: string }) =>
ticketApi.create(payload),
}
if (slots.orderId) {
const order = await tools.queryOrder(slots.orderId)
// ...
}
用 LangChain(统一工具定义 + 参数 Schema)
import { tool } from '@langchain/core/tools'
import { z } from 'zod'
// 目的:统一工具描述和入参契约,便于 Agent 自动调用与校验
const queryOrderTool = tool(
async ({ orderId }) => {
return omsApi.getOrder(orderId)
},
{
name: 'query_order',
description: '通过订单号查询订单详情',
schema: z.object({
orderId: z.string().describe('用户订单号'),
}),
},
)
const createTicketTool = tool(
async ({ orderId, policy }) => {
return ticketApi.create({ orderId, policy })
},
{
name: 'create_after_sales_ticket',
description: '创建售后工单',
schema: z.object({
orderId: z.string(),
policy: z.enum(['refund', 'replace', 'repair']),
}),
},
)
3.5 RAG 数据接入层
3.5.1 不用 LangChain(手拼版,对比用)
import fs from 'node:fs/promises'
import OpenAI from 'openai'
import { Pool } from 'pg'
// 说明:
// 1) 这里演示“不用 LangChain”时你需要自己写的核心步骤
// 2) 为了对比清晰,向量库示例用 pgvector(SQL 手写)
// 3) 真实项目还要补更多细节:重试、限流、批量写入、异常恢复、监控等
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
const db = new Pool({ connectionString: process.env.PG_DSN })
// 手写切分函数:需要你自己维护 chunkSize / overlap 规则
function splitText(text: string, chunkSize = 500, overlap = 80): string[] {
const chunks: string[] = []
let start = 0
while (start < text.length) {
const end = Math.min(start + chunkSize, text.length)
chunks.push(text.slice(start, end))
if (end === text.length) break
start = end - overlap
}
return chunks
}
async function buildIndexWithoutLangChain() {
// 1) 自己读文档
const policyText = await fs.readFile('./knowledge/after-sales-policy.txt', 'utf8')
// 2) 自己切分
const chunks = splitText(policyText, 500, 80)
// 3) 自己调用 embedding 接口并逐条入库
for (const chunk of chunks) {
const emb = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: chunk,
})
const vector = emb.data[0].embedding
// 注意:SQL / 向量格式 / 维度校验都要你自己维护
await db.query(
`INSERT INTO policy_chunks(content, embedding)
VALUES ($1, $2::vector)`,
[chunk, `[${vector.join(',')}]`],
)
}
}
async function answerWithRagWithoutLangChain(question: string) {
// 4) 自己把问题转 embedding
const qEmb = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: question,
})
const qVector = qEmb.data[0].embedding
// 5) 自己写向量检索 SQL(top-k)
const topK = await db.query(
`SELECT content
FROM policy_chunks
ORDER BY embedding <-> $1::vector
LIMIT 4`,
[`[${qVector.join(',')}]`],
)
const context = topK.rows.map((r) => r.content).join('\n\n')
// 6) 自己拼 prompt 并调 chat 接口
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: '你是售后客服助手,只能基于给定政策回答。',
},
{
role: 'user',
content: `政策上下文:\n${context}\n\n用户问题:\n${question}`,
},
],
})
return completion.choices[0]?.message?.content ?? ''
}
3.5.2 用 LangChain(同样目标,代码更聚焦)
import { TextLoader } from 'langchain/document_loaders/fs/text'
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
import { OpenAIEmbeddings, ChatOpenAI } from '@langchain/openai'
import { MemoryVectorStore } from 'langchain/vectorstores/memory'
import { ChatPromptTemplate } from '@langchain/core/prompts'
import { RunnableSequence } from '@langchain/core/runnables'
// 1) 加载政策文档(示例:本地售后政策文件)
const loader = new TextLoader('./knowledge/after-sales-policy.txt')
const docs = await loader.load()
// 2) 切分文档(避免 chunk 太大导致检索噪声)
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500,
chunkOverlap: 80,
})
const chunks = await splitter.splitDocuments(docs)
// 3) 向量化 + 建索引
const embeddings = new OpenAIEmbeddings({
model: 'text-embedding-3-small',
})
const vectorStore = await MemoryVectorStore.fromDocuments(chunks, embeddings)
// 4) 构建检索器(每次取最相关的 4 段)
const retriever = vectorStore.asRetriever(4)
// 5) 定义“检索增强回答”链路(RAG)
const llm = new ChatOpenAI({ model: 'gpt-4o-mini' })
const ragPrompt = ChatPromptTemplate.fromTemplate(`
你是售后客服助手,只能基于给定政策回答。
政策上下文:
{context}
用户问题:
{question}
请输出:
1) 命中政策(7天退/15天换/质保修)
2) 判断依据(引用上下文要点)
3) 下一步动作(补信息/建工单/转人工)
`)
const ragChain = RunnableSequence.from([
// 先检索政策片段
async (input: { question: string }) => {
const retrieved = await retriever.invoke(input.question)
const context = retrieved.map((d) => d.pageContent).join('\n\n')
return { question: input.question, context }
},
// 再让模型基于检索内容生成答案
ragPrompt,
llm,
])
// 6) 运行示例:耳机坏了场景
const result = await ragChain.invoke({
question: '上周买的耳机坏了,买了10天,应该走退货还是换货?',
})
console.log(result.content)
对比结论:
- 不用 LangChain:你得手写文档处理、切分、向量入库、检索 SQL、Prompt 拼接和调用链拼装。
- 用 LangChain:这些步骤仍然存在,但通过标准组件表达,代码更短、迁移成本更低、复用更容易。
3.6 检索增强策略层(不一定是重代码)
3.6.1 先把过程讲清楚(耳机坏了场景)
用户原话:上周买的耳机坏了
这句话直接检索,往往会“召回不全”或“召回太泛”。
所以常见增强流程是 4 步。下面逐点展开(每点都写清楚输入/处理/输出):
第 1 点:Query Rewrite(先优化检索查询)
你的理解“优化提示词”最接近这一步。
但这里优化的不是“最终回答用户的提示词”,而是“给检索系统的查询字符串”。
数据流转(字段级):
输入对象:
{
"question": "上周买的耳机坏了"
}
-> 进入 rewritePrompt(给改写模型的提示词)
-> LLM 输出 rewritten_query
输出对象:
{
"question": "上周买的耳机坏了",
"rewritten_query": "耳机 售后 故障 退换修 7天无理由 15天换货 质保 订单信息"
}
flowchart LR
A["输入对象 question\n上周买的耳机坏了"] --> B["rewritePrompt\n检索查询改写提示词"]
B --> C["LLM 改写"]
C --> D["输出对象\nquestion + rewritten_query"]
D --> E["下一步\nHybrid Search 仅使用 rewritten_query"]
关键点:
question是原始用户语句,面向人类表达。rewritten_query是面向检索系统的表达,专门加入政策锚点词。- 下一步(Hybrid Search)只吃
rewritten_query,不用原始口语句子。
为什么更好:
- 原始问题关键词稀疏,容易漏召回。
- 改写后召回目标更明确,尤其是规则词(7天/15天/质保)。
第 2 点:Hybrid Search(三路并行:关键词 + 向量 + 图检索)
只用一种检索会偏科:
- 只关键词:容易漏掉语义相近说法(“坏了” vs “性能故障”)
- 只向量:可能抓到语义相关但规则词不精确的文档
- 只图检索:当实体抽取不稳时,容易漏召回
数据流转(字段级):
输入对象:
{
"rewritten_query": "耳机 售后 故障 退换修 7天无理由 15天换货 质保 订单信息"
}
分支A(关键词检索)输出 keyword_hits:
[
{ "id": "p2", "content": "15天内性能故障可换货" },
{ "id": "p1", "content": "7天无理由退货条件" }
]
分支B(向量检索)输出 vector_hits:
[
{ "id": "p5", "content": "耳机功能异常售后处理" },
{ "id": "p3", "content": "超过15天走质保维修" }
]
分支C(图检索)输出 graph_hits:
[
{ "id": "p2", "content": "耳机 -> 购买10天 -> 15天内故障可换货" },
{ "id": "p3", "content": "耳机 -> 超过15天 -> 质保维修" }
]
合并去重输出 candidates:
{
"candidates": [p2, p1, p5, p3, ...]
}
flowchart LR
A["输入 rewritten_query"] --> B1["分支A 关键词检索"]
A --> C1["分支B 向量检索"]
A --> G1["分支C 图检索"]
B1 --> B2["Elasticsearch / OpenSearch\n(倒排索引 + BM25)"]
B2 --> B3["关键词检索结果 keyword_hits"]
C1 --> C2["Embedding 模型\n(把 query 转向量)"]
C2 --> C3["向量数据库\n(Vector DB)"]
C3 --> C4["向量检索结果 vector_hits"]
G1 --> G2["实体/关系抽取\n(商品、时效、故障类型)"]
G2 --> G3["图数据库\n(Neo4j / Neptune)"]
G3 --> G4["图检索结果 graph_hits"]
B3 --> D["合并去重 merge"]
C4 --> D
G4 --> D
D --> E["输出 candidates"]
E --> F["下一步 Re-rank"]
KB["政策语料库"] --> B2
KB --> C3
KB --> G3
关键点:
keyword_hits更擅长抓“规则硬词”(7天、15天、质保)。vector_hits更擅长抓“语义近义”(坏了=故障=功能异常)。graph_hits更擅长抓“关系约束”(耳机 + 购买10天 + 故障 => 15天换货)。candidates是下一步 Re-rank 的输入,不直接给用户。- 在生产里,关键词检索这条链路通常落在 Elasticsearch / OpenSearch。
- 在生产里,图检索通常落在 Neo4j / Neptune 等图数据库。
为什么更好:
- 召回更完整:既不漏规则条款,也不漏语义相关条款,还能保留关键关系链。
- 给 Re-rank 提供更高质量候选池。
图检索的优势(相对向量检索):
- 关系可解释:可以明确展示“实体 -> 关系 -> 规则”的证据链。
- 多跳能力强:适合“商品类别 -> 售后策略 -> 特殊例外”这类跨节点推理。
- 规则约束更稳:对“时效、类目、故障类型”这类结构化条件更友好。
什么时候并行,什么时候串行:
- 并行(默认推荐):用户输入口语化、实体不稳定、希望尽量不漏召回时。
- 串行(图先行再向量):实体抽取非常稳定,且规则强依赖关系链时。
- 串行(向量先行再图):语料很大、先用向量粗召回降成本,再用图做精筛时。
第 3 点:Re-rank(二次重排)
你这个理解可以这样说得更准确:
- 是的,这一步会用到 LLM。
- 但它不是“直接优化最终答案”,而是“先从很多检索结果里选出最该看的证据”。
通俗比喻:
- Hybrid 检索像一次海投,先拿到很多候选简历。
- Re-rank 像面试官先筛选 Top3 最匹配简历。
- 最后回答模型只看 Top3,再给用户结论。
数据流转(字段级):
输入对象:
{
"question": "上周买的耳机坏了,买了10天,应该退货还是换货?",
"candidates": [doc1, doc2, ... docN] // N 通常较大(如 20~50)
}
-> 进入 rerankPrompt(要求只按“能否回答当前问题”排序)
-> LLM 输出 ranked_ids
输出对象:
{
"ranked_ids": ["p2", "p3", "p4", ...],
"top_docs": [p2, p3, p4] // 只截取 TopK 进入下一步
}
flowchart LR
A["输入 question + candidates(N条)"] --> B["rerankPrompt\n定义排序标准"]
B --> C["LLM Re-ranker\n输出 ranked_ids"]
C --> D["按 ranked_ids 映射文档"]
D --> E["截取 TopK docs"]
E --> F["下一步 生成答案"]
示例:
用户问题:
上周买的耳机坏了,买了10天,应该退货还是换货?
候选文档(重排前):
1. 手机配件7天退货规则
2. 耳机15天内性能故障可换货
3. 耳机超过15天走维修
4. 订单号缺失补充流程
重排后 Top3:
1. 耳机15天内性能故障可换货
2. 耳机超过15天走维修
3. 订单号缺失补充流程
为什么更好:
- 把“最能回答当前问题”的条款放前面,减少无关噪声。
- 降低把错误/无关文档喂给回答模型的概率。
第 4 点:生成答案(只喂 TopK 上下文)
最后一步才是“面向用户生成答案”。
关键是:不要把全部候选都喂给模型,而是只喂重排后的 TopK(通常 3~5 条)。
示例:
输入给回答模型的上下文:
- 耳机15天内性能故障可换货
- 超过15天走质保维修
- 订单信息缺失需补全
输出给用户:
你购买约10天,若确认为性能故障,优先走“15天内换货”。
请补充订单号后我为你创建售后工单。
flowchart LR
A["输入 question + top_docs(TopK)"] --> B["answerPrompt\n要求按政策输出结论"]
B --> C["LLM 回答模型"]
C --> D["结构化结果\npolicy + reason + next_action"]
D --> E["面向用户回复\n+ 是否建工单/补信息/转人工"]
为什么更好:
- 减少“上下文污染”,降低模型一本正经答错的概率。
总结一句:
- 第 1 步优化“怎么搜”
- 第 2、3 步优化“搜什么”
- 第 4 步优化“怎么答”
3.6.2 用 LangChain 的实现代码(基于 3.5 的 llm/retriever)
import { ChatPromptTemplate } from '@langchain/core/prompts'
import { StringOutputParser } from '@langchain/core/output_parsers'
import { z } from 'zod'
/**
* 约定:
* - llm: 来自 3.5 的 ChatOpenAI 实例
* - retriever: 来自 3.5 的向量检索器(vectorStore.asRetriever)
* - POLICY_DOCS: 本地政策文档数组(用于关键词检索演示)
*/
// 1) Query Rewrite:先把用户原话改写成“检索友好”的查询
const rewritePrompt = ChatPromptTemplate.fromTemplate(`
你是检索查询改写器。请将用户问题改写成更适合检索政策文档的查询。
要求:保留售后关键条件词(7天退、15天换、质保、故障、订单信息)。
只输出改写后的查询,不要解释。
用户问题: {question}
`)
const rewriteChain = rewritePrompt.pipe(llm).pipe(new StringOutputParser())
// 2) 关键词检索(Hybrid 的 keyword 分支)
function keywordRetrieve(query: string, topK = 8) {
const tokens = new Set(
query.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ' ').split(/\s+/).filter(Boolean),
)
return POLICY_DOCS
.map((doc) => {
const dt = new Set(
doc.content.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ' ').split(/\s+/).filter(Boolean),
)
let overlap = 0
tokens.forEach((t) => {
if (dt.has(t)) overlap++
})
return { doc, score: overlap }
})
.sort((a, b) => b.score - a.score)
.filter((x) => x.score > 0)
.slice(0, topK)
.map((x) => x.doc)
}
// 3) Re-rank:对候选文档做二次排序(结构化输出 rankedIds)
const rerankSchema = z.object({
rankedIds: z.array(z.string()),
})
const rerankModel = llm.withStructuredOutput(rerankSchema)
const rerankPrompt = ChatPromptTemplate.fromTemplate(`
你是检索重排器。请根据用户问题对候选文档按相关性从高到低排序。
只返回 JSON,格式为:{"rankedIds":["id1","id2"]}。
用户问题:
{question}
候选文档(JSON):
{candidates}
`)
// 4) 把三步组合成“检索增强函数”
async function enhancedRetrieve(question: string) {
// 4.1 查询改写
const rewrittenQuery = await rewriteChain.invoke({ question })
// 4.2 向量检索 + 关键词检索(Hybrid)
const vectorHits = await retriever.invoke(rewrittenQuery)
const vectorDocs = vectorHits.map((d) => ({
id: String(d.metadata?.id ?? ''),
content: d.pageContent,
}))
const keywordDocs = keywordRetrieve(rewrittenQuery, 8)
// 4.3 合并去重
const mergedMap = new Map<string, { id: string; content: string }>()
;[...vectorDocs, ...keywordDocs].forEach((d) => mergedMap.set(d.id, d))
const candidates = Array.from(mergedMap.values())
// 4.4 二次重排
const rerankInput = await rerankPrompt.invoke({
question,
candidates: JSON.stringify(candidates, null, 2),
})
const { rankedIds } = await rerankModel.invoke(rerankInput)
const byId = new Map(candidates.map((d) => [d.id, d]))
const topDocs = rankedIds.map((id) => byId.get(id)).filter(Boolean).slice(0, 3)
return {
rewrittenQuery,
candidates,
topDocs,
}
}
// 5) 调用示例
const retrieved = await enhancedRetrieve('上周买的耳机坏了,买了10天,应该退货还是换货?')
console.log('改写查询:', retrieved.rewrittenQuery)
console.log('重排后文档:', retrieved.topDocs)
3.6.3 不用 LangChain 时你要自己做什么
不用 LangChain 也能实现同样流程,但你要手写:
- 两次以上模型调用协议(改写、重排、回答)
- 不同阶段的 JSON 解析与容错
- 向量检索 + 关键词检索 + 合并去重
- 各步骤的输入输出传递和运行日志
所以 3.6 的重点不是“有没有这三步”,而是“是否能把三步做成稳定、可复用、可调试的模块”。
3.7 链式编排层(LCEL / Runnable)
先说“链式编排解决了什么问题”(总结版):
- 解决步骤散落:把“抽取 -> 检索 -> 判定 -> 执行动作”收敛成一条可读流程。
- 解决数据传递混乱:明确每一步输入输出,减少字段丢失和隐式依赖。
- 解决治理逻辑分散:重试、日志、超时、fallback 可以挂在链上统一管理。
- 解决复用困难:把步骤做成可插拔模块,便于跨场景复用(售后、质检、审计)。
下面用“耳机坏了”同样业务,换一个更贴近真实工程的写法(pipe 风格):
import { ChatPromptTemplate } from '@langchain/core/prompts'
import { RunnableLambda } from '@langchain/core/runnables'
// Step 1: 统一输入格式(前端/接口层传什么都先归一化)
const normalizeInput = RunnableLambda.from(
async (input: { userText: string; threadId?: string }) => ({
userText: input.userText,
threadId: input.threadId ?? 'anonymous-thread',
}),
)
// Step 2: 槽位抽取(从用户话术中抽取订单号、购买天数、故障描述)
const extractPrompt = ChatPromptTemplate.fromMessages([
['system', '抽取售后槽位'],
['human', '{input}'],
])
const extractStep = RunnableLambda.from(
async (state: { userText: string; threadId: string }) => {
const slots = await extractPrompt.pipe(structuredModel).invoke({
input: state.userText,
})
return { ...state, slots }
},
)
// Step 3: 政策检索(从 RAG 检索“7天退/15天换/质保修”)
const retrieveStep = RunnableLambda.from(
async (state: {
userText: string
threadId: string
slots: { orderId?: string; buyDays?: number; issue?: string }
}) => {
const docs = await retriever.invoke(state.userText)
return { ...state, docs }
},
)
// Step 4: 决策判定(refund / replace / repair)
const decideStep = RunnableLambda.from(
async (state: {
userText: string
threadId: string
slots: { orderId?: string; buyDays?: number; issue?: string }
docs: string[]
}) => {
const decision = await decidePolicy(state.slots, state.docs)
return { ...state, decision }
},
)
// Step 5: 输出动作(给用户回复 + 是否建工单)
const buildReplyStep = RunnableLambda.from(
async (state: {
userText: string
threadId: string
slots: { orderId?: string; buyDays?: number; issue?: string }
docs: string[]
decision: 'refund' | 'replace' | 'repair'
}) => {
const reply = await buildAfterSalesReply(state.decision, state.slots)
return { ...state, reply }
},
)
// 链式编排:前一步输出作为后一步输入(你提到的核心点)
const afterSalesChain = normalizeInput
.pipe(extractStep)
.pipe(retrieveStep)
.pipe(decideStep)
.pipe(buildReplyStep)
// 运行示例(耳机坏了)
const result = await afterSalesChain.invoke({
userText: '上周买的耳机坏了,买了10天',
threadId: 't_1001',
})
console.log(result.decision) // 例如 replace
console.log(result.reply) // 给用户的话术
3.7.1 用 RxJS 心智模型理解链式编排
你的类比非常好,LCEL/Runnable 可以近似理解为:
RunnableSequence.from([...])类似pipe(...)- 每个 step 是一个“接收输入 -> 返回新对象”的函数
- 数据在步骤间流动,前一步输出就是后一步输入
3.7.1.1 你提到的 pipe vs RunnableSequence 到底什么关系
你记得没错,TS 里的 LCEL 最常见写法是 .pipe()。
RunnableSequence 不是另一套东西,而是顺序链的显式构造方式。
可以理解为:
pipe是你常写的“链式语法”RunnableSequence是这个顺序链的“对象表达”
等价示例:
// 写法A:LCEL 常见 pipe 风格
const chainByPipe = prompt.pipe(model).pipe(parser)
// 写法B:显式 RunnableSequence
const chainBySequence = RunnableSequence.from([prompt, model, parser])
两者都可以执行:
await chainByPipe.invoke(input)
await chainBySequence.invoke(input)
什么时候更偏向用哪种:
- 日常开发:
pipe更直观,写起来快。 - 动态拼装步骤(按条件增删步骤):
RunnableSequence.from([...])更方便维护。
3.7.2 你问的 4 个点(结合耳机售后)
1) 输入输出固定,为什么更容易测试和换实现
本质是给每个步骤定义“数据契约”。例如:
/**
* 第一步(抽取)后的输出:
* - text: 用户原话,例如“上周买的耳机坏了”
* - slots: 从原话里抽到的结构化字段
* - orderId: 订单号(很多时候第一轮拿不到)
* - buyDays: 购买天数(例如 10,后续决定 7天退/15天换)
* - issue: 故障描述(例如“左耳无声”)
*/
type ExtractOut = {
text: string
slots: { orderId?: string; buyDays?: number; issue?: string }
}
/**
* 第二步(检索)后的输出:
* - 继承 ExtractOut(前一步数据不能丢)
* - docs: 检索到的政策片段,例如:
* - “15天内性能故障可换货”
* - “超过15天走质保维修”
*/
type RetrieveOut = ExtractOut & { docs: string[] }
/**
* 第三步(决策)后的输出:
* - 在前两步基础上新增最终策略 decision
* - decision 是给后续动作节点用的“明确指令”
* - refund: 退货
* - replace: 换货
* - repair: 维修
*/
type DecideOut = RetrieveOut & { decision: 'refund' | 'replace' | 'repair' }
好处:
- 测试容易:测
decideStep时,可直接喂一个假的RetrieveOut,不必真的跑前两步。 - 换实现容易:把“向量检索”换成“三路检索”,只要输出仍是
docs,后面步骤不用改。
2) 复用怎么发生
复用的不是整条大链,而是“子链/步骤”。
const extractStep = async (input: { text: string }) => ({ ...input, slots: {/* ... */} })
const retrieveStep = async (input: { slots: any }) => ({ ...input, docs: [/* ... */] })
// 场景A:售后决策
const chainA = RunnableSequence.from([extractStep, retrieveStep, decideStep])
// 场景B:质检审计(复用前两步)
const chainB = RunnableSequence.from([extractStep, retrieveStep, auditStep])
3) 运行治理统一(真实场景)
场景:工单系统偶发超时,或者检索服务偶尔 500。
不用统一链路时:你要在每个函数里分散写重试、超时、日志。
用链路时:治理策略集中挂载,避免散落。
const chain = RunnableSequence.from([extractStep, retrieveStep, decideStep, createTicketStep])
.withRetry({ stopAfterAttempt: 3 }) // 统一重试策略
.withConfig({ runName: 'after-sales-chain' }) // 统一观测标识
4) 为什么说“容易演进到 LangGraph”
后续你把流程升级为“分支/回路/人工审批”时,LangChain 子链可直接作为 LangGraph 节点能力复用:
// 伪代码:LangGraph node 里复用 LangChain chain
async function policyNode(state: { text: string }) {
const result = await afterSalesChain.invoke({ text: state.text })
return { ...state, decision: result.decision }
}
这意味着不是推翻重写,而是“把已有能力拼进更复杂流程”。
3.7.3 回到你的原问题:不用 LangChain 直接写行不行
行,完全可以。
你的判断也对:在小项目里体感可能只是“代码更简洁”。
不用 LangChain(手写编排)
// 耳机售后链路:抽取 -> 检索 -> 决策 -> 建工单
// 问题:重试/日志/错误处理会散在每一步里
async function handleAfterSalesWithoutLangChain(text: string) {
let slots
try {
slots = await extractSlots(text) // 你自己调模型
} catch (e) {
logger.error('extractSlots failed', e)
throw e
}
let docs
try {
docs = await retrievePolicyDocs(text) // 你自己拼检索
} catch (e) {
logger.error('retrievePolicyDocs failed', e)
// 手写重试逻辑(只在这一段生效,其他段要重复写)
docs = await retrievePolicyDocs(text)
}
const decision = await decidePolicy(slots, docs)
try {
return await createTicket(decision)
} catch (e) {
logger.error('createTicket failed', e)
// 降级策略也要你自己在这里写
return { status: 'fallback_to_human' }
}
}
用 LangChain(链式编排)
// 同样的业务步骤,但用统一链路表达
const afterSalesChain = RunnableSequence.from([
extractStep,
retrieveStep,
decideStep,
createTicketStep,
])
.withRetry({ stopAfterAttempt: 3 }) // 统一重试策略
.withConfig({ runName: 'after-sales-chain' }) // 统一观测标识
const result = await afterSalesChain.invoke({ text: '上周买的耳机坏了' })
不用的后果(真实项目会放大)
- 步骤一多,重试/日志/降级逻辑会分散在多个函数中,维护成本上升。
- 新增一个步骤时,容易漏掉治理逻辑(比如某一步没加超时或没打日志)。
- 排障时要跨多个函数追踪上下文,问题定位时间更长。
- 迁移到更复杂流程(分支、回路、人工审批)时,改造面更大。
3.8 多轮上下文与记忆层
先讲一个真实场景(什么叫多轮对话):
第1轮 用户: 上周买的耳机坏了
第1轮 系统: 请提供订单号
第2轮 用户: 订单号 123
第2轮 系统: 请问购买了几天,故障现象是什么?
第3轮 用户: 买了10天,左耳没声音
第3轮 系统: 命中“15天内性能故障可换货”,我先为你创建换货工单
这就是多轮:用户不会一次把信息说全,系统要边问边补齐状态。
3.8.1 用“回路”来理解多轮(这就是 LangGraph 的回边)
你说得完全对,这里本质就是回路:
extract(抽取信息)- 如果缺字段 ->
clarify(追问) - 用户回答后再回到
extract
也就是:extract -> clarify -> extract,直到字段补齐。
flowchart LR
A["用户输入\n上周买的耳机坏了"] --> B["extract\n抽取槽位"]
B --> C{"字段齐了吗?\norderId + buyDays + issue"}
C -- "否" --> D["clarify\n追问缺失字段"]
D --> A2["用户补充\n订单号/购买天数/故障"]
A2 --> B
C -- "是" --> E["决策\nrefund/replace/repair"]
一眼看状态怎么累积:
第1轮后: { issue: "耳机坏了", orderId: null, buyDays: null }
第2轮后: { issue: "耳机坏了", orderId: "123", buyDays: null }
第3轮后: { issue: "左耳没声音", orderId: "123", buyDays: 10 }
-> 字段补齐,进入退/换/修判定
3.8.2 极简代码(不讲框架细节,只看回路)
type State = {
orderId?: string
buyDays?: number
issue?: string
}
function isComplete(s: State) {
return !!(s.orderId && s.buyDays !== undefined && s.issue)
}
// 每轮用户发言都会调用一次(在线客服常见模式)
async function handleTurn(sessionId: string, userText: string) {
// 1) 读历史状态(上轮累积结果)
const state = (await loadState(sessionId)) ?? {}
// 2) 从本轮话术里继续抽取字段并合并
const more = await extractSlots(userText) // 例如抽出 orderId/buyDays/issue
const merged = { ...state, ...more }
// 3) 字段不全 -> 追问(回路继续)
if (!isComplete(merged)) {
await saveState(sessionId, merged)
return askMissingField(merged) // 例如“请提供订单号”
}
// 4) 字段齐全 -> 执行决策
const decision = await decidePolicy(merged)
await saveState(sessionId, { ...merged, decision })
return buildReply(decision)
}
这段代码对应的就是上面的回路图。
LangChain/LangGraph 的价值,是把这个模式做成更标准、可复用、可观测。
3.8.3 记忆层到底是什么(你这个理解是对的)
你说的“通过不停累加多轮上下文”,本质就是记忆层在工作。
但在工程里通常会拆成 3 类记忆:
-
state(流程状态记忆)
用于业务决策字段,例如:orderId/buyDays/issue/decision。
目标是“流程能继续跑”。 -
history(对话短期记忆)
保存最近几轮对话原文。
目标是“说话不失忆、语义连贯”。 -
profile(长期记忆,可选)
保存跨会话的用户偏好或长期信息,例如“常用收货地址、偏好联系时间”。
目标是“跨会话个性化”。
flowchart LR
A["新一轮用户输入"] --> B["读取记忆\nstate + history + profile"]
B --> C["模型与规则处理"]
C --> D["输出本轮回复/动作"]
D --> E["写回记忆\n更新 state/history/profile"]
E --> A2["下一轮继续"]
可以简单理解为:
- 回路解决“流程怎么走”
- 记忆层解决“上轮信息怎么留到下轮”
3.8.4 这一层解决的核心问题
- 避免重复提问:系统知道“订单号已经给过了”。
- 避免上下文断裂:第 3 轮还能记得第 1 轮的“耳机坏了”。
- 降低误判:决策基于累积状态,而不是单轮片段信息。
3.8.5 不做会怎样(常见后果)
- 每轮都像新会话:反复问订单号,用户体验差。
- 字段易丢失:第 1 轮提到的故障信息在第 3 轮丢了。
- 决策不稳定:同一用户不同轮次得到冲突结论。
3.8.6 LangChain vs LangGraph:记忆与检查点(Checkpoint)各管什么
这是最容易混淆的点,可以这样记:
- LangChain 更偏“对话记忆能力层”(让模型记得上下文)。
- LangGraph 更偏“流程状态与恢复层”(让流程能中断后继续)。
| 维度 | LangChain | LangGraph |
|---|---|---|
| 主要目标 | 让模型在多轮里“记得说过什么” | 让流程在多节点里“记得跑到哪一步” |
| 典型数据 | history(消息历史) | state + nextNode + status |
| 关注点 | 语义连贯、减少重复提问 | 可中断、可恢复、可审计 |
| 中断恢复 | 不是核心强项 | 核心能力(配 checkpointer) |
| 典型标识 | sessionId | threadId |
一句话:
- LangChain 记“聊天上下文”。
- LangGraph 记“流程进度和状态快照”。
A) LangChain 在记忆层的典型做法(会话历史)
// 伪代码:按 sessionId 读取/写入历史,让下一轮带上前文
const history = await loadChatHistory(sessionId) // 例如 Redis/DB
const response = await historyAwarePrompt.pipe(llm).invoke({
history, // 上下文记忆
input: userText, // 本轮输入
})
await appendChatHistory(sessionId, userText, String(response.content))
它解决的是:用户第 2 轮说“订单号是123”,第 3 轮系统还能记住第 1 轮“耳机坏了”。
B) LangGraph 在检查点层的典型做法(流程恢复)
// 伪代码:按 threadId 保存/恢复流程执行位置
// state 里可放 slots、decision、审批结果等
await saveCheckpoint({
threadId: 't_1001',
nextNode: 'approval', // 当前停在哪个节点
state: { orderId: '123', buyDays: 10, issue: '左耳没声音' },
status: 'WAITING_APPROVAL',
})
// 2小时后审批回调
const cp = await loadCheckpoint('t_1001')
cp.state.approved = true
cp.status = 'RUNNING'
await saveCheckpoint(cp)
await resumeFromNode(cp.nextNode, cp.state) // 从中断点继续,不重跑全流程
它解决的是:人工审批/服务重启后,流程还能从原步骤继续。
C) 在你的耳机售后场景,建议怎么搭配
- 只多轮对话(简单问答):LangChain 记忆层通常够用。
- 涉及审批、回路、恢复、审计:要加 LangGraph checkpoint。
- 最常见生产方案:LangChain 管“会话记忆”,LangGraph 管“流程记忆(检查点)”。
3.8.7 上下文召回、上下文压缩、上下文爆炸:三者是什么
先用通俗定义:
- 上下文召回:从“很多历史信息”里只拿当前问题真正需要的部分。
- 上下文压缩:把长上下文压成短摘要/关键槽位,减少 token 占用。
- 上下文爆炸:上下文越来越长,导致成本高、延迟高、噪声高、回答变差。
在耳机售后里的典型表现:
- 第 1~8 轮混入很多寒暄和无关描述。
- 真正关键的只有:
orderId=123、buyDays=10、issue=左耳无声。 - 如果不召回/压缩,模型会被无关上下文“淹没”。
LangChain / LangGraph / Deep Agents 怎么应对
| 框架 | 主要应对点 | 常见方案 | 成熟度(落地体感) |
|---|---|---|---|
| LangChain | 召回 + 压缩(能力层) | 历史裁剪、摘要记忆、检索式记忆、Contextual Compression Retriever、重排 | 高(组件成熟、组合灵活) |
| LangGraph | 爆炸治理 + 恢复(流程层) | Checkpoint、状态分层(state/history/profile)、节点内摘要、只在关键节点注入上下文、中断恢复 | 高(生产流程友好) |
| Deep Agents | 长程任务自动上下文管理 | 自主上下文压缩、任务分解后局部上下文、阶段性摘要回写 | 中-高(思路先进,具体落地依赖实现版本) |
你可以这样记:
- LangChain 擅长“怎么处理上下文内容”。
- LangGraph 擅长“上下文在哪些流程节点被读取/写回/恢复”。
- Deep Agents 更强调“长任务里自动管理上下文”的策略。
Deep Agents 这块你是否“完全不用管”
可以这样理解:
- 对,默认有内建自动机制(压缩、摘要、offloading),你不必从零手写整套。
- 但不等于完全不管:你仍要做策略配置和边界约束。
你仍需要决定的常见事项:
- 哪些信息必须保留原文(不能只留摘要)。
- 哪些内容允许被压缩/卸载(例如大工具输出)。
- 什么时候触发人工介入(摘要质量不达标、关键信息疑似丢失)。
一句话:
- Deep Agents 是“自动化为主”。
- 生产落地仍需“策略兜底 + 监控审计”。
实操建议(你这个项目可直接用)
- 先做“召回”再做“压缩”,不要一上来全量摘要。
- 会话里只保留关键槽位和最近 N 轮原文,其余转摘要。
- 在 LangGraph 节点上加 checkpoint,避免重启后重算全量上下文。
- 设定上下文预算(例如超过阈值就触发压缩)。
- 把“摘要版本号 + 生成时间”写入状态,便于审计和回放。
3.8.8 一页总结:3个框架在“上下文召回/压缩/防爆炸”上的 API 与示例
先给结论(你可以直接复述给团队):
- LangChain:偏“内容处理层”,有较直接的记忆/摘要/裁剪能力可调用。
- LangGraph:偏“流程状态层”,核心是
thread + checkpoint + state,压缩策略常放在节点里执行。 - Deep Agents:偏“长任务自动管理层”,内建自动压缩/摘要/offloading,但仍要做策略边界配置。
A) LangChain(会话记忆 + 压缩/裁剪)
典型 API 方向:
ChatPromptTemplate+MessagesPlaceholder(把历史消息接入提示词)- 中间件/策略(例如摘要压缩、消息裁剪)
- 长短期记忆接口(按 session/thread 读写)
重点说明:下面示例里的
trimOrSummarize(history)是“示意占位函数名”,不是 LangChain 官方内置同名 API。
实际落地时,你需要:
- 自己实现该函数,或
- 用 LangChain 的现成能力(消息裁剪/摘要中间件/记忆组件)组合后封装成这个函数。
// 示例:历史接入 + 压缩策略(示意)
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts'
const prompt = ChatPromptTemplate.fromMessages([
['system', '你是售后助手,基于历史对话补全信息'],
new MessagesPlaceholder('history'),
['human', '{input}'],
])
const history = await loadChatHistory(sessionId) // Redis/DB
const response = await prompt.pipe(llm).invoke({
history: trimOrSummarize(history), // 这里放裁剪/摘要策略
input: userText,
})
await appendChatHistory(sessionId, userText, String(response.content))
适用:多轮对话语义连贯、减少重复提问。
B) LangGraph(检查点 + 恢复 + 节点内压缩)
典型 API 方向:
thread_id(会话流程标识)checkpointer(保存状态快照)getState/getStateHistory(查看当前与历史状态)
// 示例:流程检查点与恢复(示意)
const config = { configurable: { thread_id: 'ticket_1001' } }
// 首次运行
await app.invoke({ userText: '上周买的耳机坏了' }, config)
// 查看当前状态(卡在哪)
const now = await app.getState(config)
// 人工审批后恢复
await app.invoke({ approved: true }, config) // 从已有 thread 状态继续
适用:有回路、人工审批、中断恢复、可审计流程。
C) Deep Agents(自动上下文管理)
典型 API 方向:
createDeepAgent(...)- 内建 context compression / offloading / summarization(自动触发)
memory/skills/ runtime context 配置
// 示例:Deep Agents 创建(示意)
import { createDeepAgent } from 'deepagents'
const agent = await createDeepAgent({
model: 'claude-sonnet-4-6',
memory: ['/project/AGENTS.md'],
skills: ['/skills/customer-service/'],
})
const result = await agent.invoke({
messages: [{ role: 'user', content: '处理一个长会话售后任务' }],
})
适用:长流程任务、上下文易爆炸、希望默认自动治理。
D) 最常见落地组合(推荐)
- LangChain 处理“上下文内容”(召回、压缩、摘要)。
- LangGraph 管“流程记忆”(checkpoint 与恢复)。
- Deep Agents 适合需要更强自动化上下文管理的长任务场景。
3.9 可观测与评估层(LangSmith + Langfuse)
你线上一定会被问:
- 为什么没走换货?
- 卡在哪一步?
- 哪个模型/哪个 prompt 导致误判?
- 哪次发布后错误率上升?
所以可观测层的目标不是“看日志”,而是“能定位原因、能做回归评估、能持续优化”。
3.9.1 LangSmith vs Langfuse(怎么选)
| 维度 | LangSmith | Langfuse |
|---|---|---|
| 与 LangChain/LangGraph 集成 | 深度集成,LangChain 可零改动起步(环境变量启用) | 集成成熟(Callback / OTel),但通常需要显式接入 |
| Tracing 体验 | 对 LangChain/LangGraph trace 树很友好 | 通用 tracing 能力强,跨框架可观测也好做 |
| Evals 评估能力 | 原生评估与数据集闭环较完整 | 也支持评估与打分,但实现路径更偏通用平台化 |
| 部署形态 | 托管体验强 | 开源/自建友好(很多团队看重这点) |
| 适合场景 | LangChain/LangGraph 主栈,想快速起量 | 多框架混用或偏好自建可观测平台 |
一句话建议:
- 你当前这份文档主线是 LangChain/LangGraph,优先 LangSmith 上手最快。
- 如果你团队强调开源自建与统一观测底座,Langfuse 是强备选。
3.9.2 LangSmith 典型 API/配置(JS/TS)
官方最常见起步方式:环境变量启用 tracing,LangChain 代码可保持不变。
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=<your-api-key>
export LANGSMITH_PROJECT=after-sales-agent
export OPENAI_API_KEY=<your-openai-api-key>
// 代码可保持 LangChain 常规写法,trace 自动进入 LangSmith
const result = await afterSalesChain.invoke({ text: '上周买的耳机坏了' })
可选:给链路加 tags/metadata,方便查询与对比:
await afterSalesChain.invoke(
{ text: '上周买的耳机坏了' },
{
tags: ['after-sales', 'headset'],
metadata: { env: 'prod', feature: 'policy-v2' },
},
)
3.9.3 Langfuse 典型 API/配置(JS/TS)
Langfuse 常见接入方式是 OTel + Langfuse 处理器,或对 OpenAI/LangChain 做包装。
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.com
// 1) 先初始化 OTel + Langfuse span processor
import { NodeSDK } from '@opentelemetry/sdk-node'
import { LangfuseSpanProcessor } from '@langfuse/otel'
const sdk = new NodeSDK({
spanProcessors: [new LangfuseSpanProcessor()],
})
sdk.start()
// 2) LangChain 场景:挂 callback
import { CallbackHandler } from '@langfuse/langchain'
const langfuseHandler = new CallbackHandler()
const result = await afterSalesChain.invoke(
{ text: '上周买的耳机坏了' },
{ callbacks: [langfuseHandler] },
)
3.9.4 结合耳机售后,最小落地建议
- 每次调用都打上
threadId/orderId/env/version元数据。 - 重点追踪 4 个节点耗时:抽取、检索、重排、决策。
- 为“误判退换修”建立评估集,发布前后自动回归。
- 对人工审批链路单独做 trace 过滤视图(便于排障和审计)。
3.9.5 收费与数据安全:你最关心的结论
A) 收费(按“框架本体”与“平台服务”区分)
| 项目 | 结论(简版) | 你该怎么理解 |
|---|---|---|
| LangChain(开源框架) | 框架本体开源可用 | 成本主要来自模型调用、向量库、基础设施,不是“买 LangChain 才能跑” |
| LangSmith(托管平台) | 有免费/付费计划,通常按功能+用量计费 | 适合希望快速接入观测与评估的团队 |
| Langfuse Cloud | 有免费/付费计划 | 快速上云,功能开箱 |
| Langfuse Self-Hosted | 可自托管(常见 OSS 路线) | 可把观测数据留在自己基础设施内 |
价格会随时间调整,落地前请以官方 pricing 页面为准。
B) 数据安全(按“数据驻留位置”看)
| 方案 | 数据主要在哪里 | 安全责任 |
|---|---|---|
| LangSmith 托管 | 平台云端(按你配置与服务条款) | 平台 + 你方共同责任(分类、脱敏、权限) |
| Langfuse Cloud | Langfuse 云端 | 平台 + 你方共同责任 |
| Langfuse Self-Hosted | 你自己的 VPC/私有云/本地机房 | 你方主责(补丁、网络、密钥、备份、审计) |
一句话:
- 想快:托管平台(LangSmith / Langfuse Cloud)。
- 想强数据主权:Langfuse 自托管(但运维责任更重)。
C) 合规与安全落地清单(强烈建议写进项目规范)
- 对 trace 输入做脱敏:手机号、邮箱、地址、身份证、支付信息默认打码。
- 只记录必要字段:调试够用即可,避免把原始敏感文本全量上报。
- 设置数据保留策略:生产与测试环境分开,过期自动清理。
- 做权限隔离:按项目/租户/环境分隔可见性。
- 为关键链路保留审计字段:
threadId / orderId / version / reviewer。 - 上线前做“红线检查”:确认不会把密钥、token、内部 URL 记入 trace。
这层不是“可选锦上添花”,而是生产质量的基础设施。
3.10 运行治理层(重试/超时/降级/缓存)
// 说明:不同版本 API 细节会有差异,这里是工程意图示例
// 目的:让调用链更稳,避免外部依赖抖动直接传递给用户
const resilientChain = afterSalesChain
// 失败自动重试(示意)
.withRetry({ stopAfterAttempt: 3 })
// 统一运行配置(示意)
.withConfig({ runName: 'after-sales-chain' })
这一层是“生产可用性”的关键,不是可有可无。
4. 用与不用 LangChain 的整体对比(耳机售后 Agent)
| 维度 | 不用 LangChain | 用 LangChain |
|---|---|---|
| 模型切换 | 改多处 SDK 调用 | 多数只改模型初始化 |
| Prompt 管理 | 字符串散落 | 模板化可复用 |
| 输出稳定性 | 手写 JSON parse | Schema 化输出 |
| 工具接入 | 协议各写各的 | Tool + Schema 统一 |
| RAG 接入 | 重复造轮子 | 标准组件拼装 |
| 多轮会话 | 易丢上下文 | 历史接入更自然 |
| 排障与评估 | 数据碎片化 | 链路可追踪性更好 |
| 维护成本 | 规则变多后快速上升 | 模块化演进更稳 |
5. 最后一句(给团队沟通时可以直接用)
开发 Agent 一定会碰到模型、Prompt、输出、工具、RAG、上下文、治理、观测等模块。
LangChain 的价值不是替你做业务决策,而是把这些模块做成统一中间层,像 JDBC 一样屏蔽底层差异。
你把时间花在“耳机售后规则怎么判”上,而不是花在“每个厂商 SDK 都写一遍”上。