LangChain 在 Agent 开发中的定位:10 个模块(含代码对比,耳机售后案例)

0. 先说核心结论:LangChain 到底解决了什么问题

在一个真实 Agent(比如售后客服 Agent)里,你会同时遇到这些工程问题:

  • 模型厂商 SDK 不统一(OpenAI / Anthropic / Gemini / 本地模型)
  • Prompt 组织混乱(系统提示词、用户提示词、变量注入)
  • 输出不稳定(有时 JSON,有时自然语言)
  • 工具调用协议不统一(查订单、查质保、建工单)
  • RAG 流程重复造轮子(加载、切分、向量化、检索)
  • 多轮上下文难维护(用户分几轮补信息)
  • 线上稳定性治理(超时、重试、降级)
  • 追踪排障困难(为什么没走换货)

LangChain 的定位就是:把这些"重复底层工程"做成统一中间层

你可以把它类比为 AI 应用里的 JDBC + 调用链框架 + 统一工具协议层


1. 统一业务场景(贯穿全文)

用户第一句话:上周我买的一个耳机坏了

Agent 要完成:

  1. 识别意图(售后/其他)
  2. 抽取槽位(订单号、购买时间、故障描述、是否在保)
  3. 缺信息就追问(多轮)
  4. 检索并命中政策(7天退/15天换/质保修)
  5. 调用工具(查订单、建工单)
  6. 输出结构化结论给下游系统

2. 十个模块总览(先看全貌)

  1. 模型标准层(Model Abstraction)
  2. Prompt 工程层(Prompt Templates)
  3. 输出控制层(Structured Output)
  4. 工具调用层(Tools)
  5. RAG 数据接入层(Load/Split/Embed/Retrieve)
  6. 检索增强策略层(Re-rank / Hybrid / Query Rewrite)
  7. 链式编排层(LCEL / Runnable)
  8. 多轮上下文与记忆层(Message History / Memory)
  9. 可观测与评估层(Tracing / Eval)
  10. 运行治理层(Retry / Timeout / Fallback / Cache)

说明:并不是 10 点都必须写很多代码;有些是"能力层与工程策略"。


3. 模块展开(按"要解决什么 -> LangChain 怎么做 -> 代码对比")

3.1 模型标准层(像 JDBC)

3.1.1 你自己直接写(不用 LangChain)

ts 复制代码
// 目的:对不同厂商模型做统一调用
// 问题:你要自己处理不同 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

ts 复制代码
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(字符串拼接,易散乱)

ts 复制代码
// 问题:提示词散落,变量注入靠手拼,容易漏字段/格式不一致
const prompt = `
你是售后助手。
规则:先识别意图,再抽取订单号/购买时间/故障描述。
用户输入:${userText}
`

用 LangChain(模板化管理)

ts 复制代码
import { ChatPromptTemplate } from '@langchain/core/prompts'

// 目的:把系统规则和变量输入显式化,便于复用/评审/版本管理
const classifyPrompt = ChatPromptTemplate.fromMessages([
  ['system', '你是售后助手。先识别意图,再抽取槽位。'],
  ['human', '用户输入:{input}'],
])

const formatted = await classifyPrompt.invoke({ input: '上周买的耳机坏了' })

3.3 输出控制层(结构化输出)

不用 LangChain(手动 JSON 解析 + 手动兜底)

ts 复制代码
const raw = await callModelWithoutLangChain('openai', prompt)

let data: any
try {
  // 手动解析,遇到"半 JSON"时很脆弱
  data = JSON.parse(raw)
} catch {
  // 你还得手写重试或修复逻辑
  throw new Error('模型输出不是合法 JSON')
}

用 LangChain(Schema 约束输出)

ts 复制代码
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(你自己维护"工具协议")

ts 复制代码
// 问题:调用哪个工具、参数是否合法、失败如何处理,全部手写
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)

ts 复制代码
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(手拼版,对比用)

ts 复制代码
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(同样目标,代码更聚焦)

ts 复制代码
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(先优化检索查询)

你的理解"优化提示词"最接近这一步。

但这里优化的不是"最终回答用户的提示词",而是"给检索系统的查询字符串"。

数据流转(字段级):

text 复制代码
输入对象:
{
  "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 "性能故障")
  • 只向量:可能抓到语义相关但规则词不精确的文档
  • 只图检索:当实体抽取不稳时,容易漏召回

数据流转(字段级):

text 复制代码
输入对象:
{
  "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,再给用户结论。

数据流转(字段级):

text 复制代码
输入对象:
{
  "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["下一步 生成答案"]

示例:

text 复制代码
用户问题:
上周买的耳机坏了,买了10天,应该退货还是换货?

候选文档(重排前):
1. 手机配件7天退货规则
2. 耳机15天内性能故障可换货
3. 耳机超过15天走维修
4. 订单号缺失补充流程

重排后 Top3:
1. 耳机15天内性能故障可换货
2. 耳机超过15天走维修
3. 订单号缺失补充流程

为什么更好:

  • 把"最能回答当前问题"的条款放前面,减少无关噪声。
  • 降低把错误/无关文档喂给回答模型的概率。

第 4 点:生成答案(只喂 TopK 上下文)

最后一步才是"面向用户生成答案"。

关键是:不要把全部候选都喂给模型,而是只喂重排后的 TopK(通常 3~5 条)。

示例:

text 复制代码
输入给回答模型的上下文:
- 耳机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)

ts 复制代码
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 风格):

ts 复制代码
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 是这个顺序链的"对象表达"

等价示例:

ts 复制代码
// 写法A:LCEL 常见 pipe 风格
const chainByPipe = prompt.pipe(model).pipe(parser)

// 写法B:显式 RunnableSequence
const chainBySequence = RunnableSequence.from([prompt, model, parser])

两者都可以执行:

ts 复制代码
await chainByPipe.invoke(input)
await chainBySequence.invoke(input)

什么时候更偏向用哪种:

  • 日常开发:pipe 更直观,写起来快。
  • 动态拼装步骤(按条件增删步骤):RunnableSequence.from([...]) 更方便维护。

3.7.2 你问的 4 个点(结合耳机售后)

1) 输入输出固定,为什么更容易测试和换实现

本质是给每个步骤定义"数据契约"。例如:

ts 复制代码
/**
 * 第一步(抽取)后的输出:
 * - 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) 复用怎么发生

复用的不是整条大链,而是"子链/步骤"。

ts 复制代码
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。

不用统一链路时:你要在每个函数里分散写重试、超时、日志。

用链路时:治理策略集中挂载,避免散落。

ts 复制代码
const chain = RunnableSequence.from([extractStep, retrieveStep, decideStep, createTicketStep])
  .withRetry({ stopAfterAttempt: 3 }) // 统一重试策略
  .withConfig({ runName: 'after-sales-chain' }) // 统一观测标识

4) 为什么说"容易演进到 LangGraph"

后续你把流程升级为"分支/回路/人工审批"时,LangChain 子链可直接作为 LangGraph 节点能力复用:

ts 复制代码
// 伪代码: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(手写编排)

ts 复制代码
// 耳机售后链路:抽取 -> 检索 -> 决策 -> 建工单
// 问题:重试/日志/错误处理会散在每一步里
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(链式编排)

ts 复制代码
// 同样的业务步骤,但用统一链路表达
const afterSalesChain = RunnableSequence.from([
  extractStep,
  retrieveStep,
  decideStep,
  createTicketStep,
])
  .withRetry({ stopAfterAttempt: 3 }) // 统一重试策略
  .withConfig({ runName: 'after-sales-chain' }) // 统一观测标识

const result = await afterSalesChain.invoke({ text: '上周买的耳机坏了' })

不用的后果(真实项目会放大)

  • 步骤一多,重试/日志/降级逻辑会分散在多个函数中,维护成本上升。
  • 新增一个步骤时,容易漏掉治理逻辑(比如某一步没加超时或没打日志)。
  • 排障时要跨多个函数追踪上下文,问题定位时间更长。
  • 迁移到更复杂流程(分支、回路、人工审批)时,改造面更大。

3.8 多轮上下文与记忆层

先讲一个真实场景(什么叫多轮对话):

text 复制代码
第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"]

一眼看状态怎么累积:

text 复制代码
第1轮后: { issue: "耳机坏了", orderId: null, buyDays: null }
第2轮后: { issue: "耳机坏了", orderId: "123", buyDays: null }
第3轮后: { issue: "左耳没声音", orderId: "123", buyDays: 10 }
-> 字段补齐,进入退/换/修判定

3.8.2 极简代码(不讲框架细节,只看回路)

ts 复制代码
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 类记忆:

  1. state(流程状态记忆)

    用于业务决策字段,例如:orderId/buyDays/issue/decision

    目标是"流程能继续跑"。

  2. history(对话短期记忆)

    保存最近几轮对话原文。

    目标是"说话不失忆、语义连贯"。

  3. 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 在记忆层的典型做法(会话历史)

ts 复制代码
// 伪代码:按 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 在检查点层的典型做法(流程恢复)

ts 复制代码
// 伪代码:按 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=123buyDays=10issue=左耳无声
  • 如果不召回/压缩,模型会被无关上下文"淹没"。

LangChain / LangGraph / Deep Agents 怎么应对

框架 主要应对点 常见方案 成熟度(落地体感)
LangChain 召回 + 压缩(能力层) 历史裁剪、摘要记忆、检索式记忆、Contextual Compression Retriever、重排 高(组件成熟、组合灵活)
LangGraph 爆炸治理 + 恢复(流程层) Checkpoint、状态分层(state/history/profile)、节点内摘要、只在关键节点注入上下文、中断恢复 高(生产流程友好)
Deep Agents 长程任务自动上下文管理 自主上下文压缩、任务分解后局部上下文、阶段性摘要回写 中-高(思路先进,具体落地依赖实现版本)

你可以这样记:

  • LangChain 擅长"怎么处理上下文内容"。
  • LangGraph 擅长"上下文在哪些流程节点被读取/写回/恢复"。
  • Deep Agents 更强调"长任务里自动管理上下文"的策略。

Deep Agents 这块你是否"完全不用管"

可以这样理解:

  • 对,默认有内建自动机制(压缩、摘要、offloading),你不必从零手写整套。
  • 但不等于完全不管:你仍要做策略配置和边界约束。

你仍需要决定的常见事项:

  1. 哪些信息必须保留原文(不能只留摘要)。
  2. 哪些内容允许被压缩/卸载(例如大工具输出)。
  3. 什么时候触发人工介入(摘要质量不达标、关键信息疑似丢失)。

一句话:

  • Deep Agents 是"自动化为主"。
  • 生产落地仍需"策略兜底 + 监控审计"。

实操建议(你这个项目可直接用)

  1. 先做"召回"再做"压缩",不要一上来全量摘要。
  2. 会话里只保留关键槽位和最近 N 轮原文,其余转摘要。
  3. 在 LangGraph 节点上加 checkpoint,避免重启后重算全量上下文。
  4. 设定上下文预算(例如超过阈值就触发压缩)。
  5. 把"摘要版本号 + 生成时间"写入状态,便于审计和回放。

3.8.8 一页总结:3个框架在"上下文召回/压缩/防爆炸"上的 API 与示例

先给结论(你可以直接复述给团队):

  • LangChain:偏"内容处理层",有较直接的记忆/摘要/裁剪能力可调用。
  • LangGraph:偏"流程状态层",核心是 thread + checkpoint + state,压缩策略常放在节点里执行。
  • Deep Agents:偏"长任务自动管理层",内建自动压缩/摘要/offloading,但仍要做策略边界配置。

A) LangChain(会话记忆 + 压缩/裁剪)

典型 API 方向:

  • ChatPromptTemplate + MessagesPlaceholder(把历史消息接入提示词)
  • 中间件/策略(例如摘要压缩、消息裁剪)
  • 长短期记忆接口(按 session/thread 读写)

重点说明:下面示例里的 trimOrSummarize(history) 是"示意占位函数名",不是 LangChain 官方内置同名 API。

实际落地时,你需要:

  1. 自己实现该函数,或
  2. 用 LangChain 的现成能力(消息裁剪/摘要中间件/记忆组件)组合后封装成这个函数。
ts 复制代码
// 示例:历史接入 + 压缩策略(示意)
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(查看当前与历史状态)
ts 复制代码
// 示例:流程检查点与恢复(示意)
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 配置
ts 复制代码
// 示例: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) 最常见落地组合(推荐)

  1. LangChain 处理"上下文内容"(召回、压缩、摘要)。
  2. LangGraph 管"流程记忆"(checkpoint 与恢复)。
  3. 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 代码可保持不变。

bash 复制代码
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>
ts 复制代码
// 代码可保持 LangChain 常规写法,trace 自动进入 LangSmith
const result = await afterSalesChain.invoke({ text: '上周买的耳机坏了' })

可选:给链路加 tags/metadata,方便查询与对比:

ts 复制代码
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 做包装。

bash 复制代码
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.com
ts 复制代码
// 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()
ts 复制代码
// 2) LangChain 场景:挂 callback
import { CallbackHandler } from '@langfuse/langchain'

const langfuseHandler = new CallbackHandler()
const result = await afterSalesChain.invoke(
  { text: '上周买的耳机坏了' },
  { callbacks: [langfuseHandler] },
)

3.9.4 结合耳机售后,最小落地建议

  1. 每次调用都打上 threadId/orderId/env/version 元数据。
  2. 重点追踪 4 个节点耗时:抽取、检索、重排、决策。
  3. 为"误判退换修"建立评估集,发布前后自动回归。
  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) 合规与安全落地清单(强烈建议写进项目规范)

  1. 对 trace 输入做脱敏:手机号、邮箱、地址、身份证、支付信息默认打码。
  2. 只记录必要字段:调试够用即可,避免把原始敏感文本全量上报。
  3. 设置数据保留策略:生产与测试环境分开,过期自动清理。
  4. 做权限隔离:按项目/租户/环境分隔可见性。
  5. 为关键链路保留审计字段:threadId / orderId / version / reviewer
  6. 上线前做"红线检查":确认不会把密钥、token、内部 URL 记入 trace。

这层不是"可选锦上添花",而是生产质量的基础设施。


3.10 运行治理层(重试/超时/降级/缓存)

ts 复制代码
// 说明:不同版本 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 都写一遍"上。

相关推荐
ouzz4 小时前
使用纯canvas绘制一个掘金首页
前端·canvas
乐乐同学yorsal4 小时前
一个 TypeScript 写的图片视频处理工具箱,吊打一切付费软件!
前端·命令行
lzhdim4 小时前
SQL 入门 10:SQL 内置函数:数值、字符串与时间处理
前端·数据库·sql
jstopo网站4 小时前
水厂水泵工作流程图canvas动画
前端·javascript
张元清4 小时前
5 分钟用 Vite SSR 搭建一个全栈 React 应用
前端·javascript·面试
空中海4 小时前
6.1 主题与暗色模式
运维·服务器·前端·flutter
踩着两条虫4 小时前
效率翻倍!AI智能体深度解析:自然语言 → DSL → Vue组件
前端·人工智能·低代码
吴声子夜歌5 小时前
Vue3——条件判断指令
前端·es6
snow_yan5 小时前
AI 对话流式输出: 实现“逐字丝滑、不闪烁、不卡顿”的打字机效果
前端·react.js·openai