1. 这不是另一个“AI框架”——LangChain 是大模型时代的操作系统内核

你第一次听说 LangChain,大概率是在某个技术群里看到有人发链接:“快看,这个能连本地大模型!”或者在招聘JD里刷到“熟悉 LangChain 者优先”。但如果你真去翻它的 GitHub 主页、读官方文档第一行,会发现它压根没说自己是“框架”——它写的是: LangChain is a framework for developing applications powered by large language models. (一个为大语言模型驱动的应用而生的框架)。注意这个定语:“为……而生”,不是“用来调用模型”,而是“为构建应用而生”。

我从 2023 年初就开始用 LangChain 搭第一个内部知识库,到现在手上有 7 个生产环境项目跑着不同形态的 LangChain 应用:有给法务团队做的合同条款比对助手,有嵌入 ERP 系统的采购询价自动回复模块,还有给客服坐席用的实时话术推荐弹窗。这三年踩过的坑、重写的链、重构的记忆模块,让我越来越确信一件事: LangChain 的本质,不是让你更快地调用一次 GPT,而是帮你把“大模型能力”像水电一样,接入你已有的业务系统里。 它解决的从来不是“怎么让 AI 说话”,而是“怎么让 AI 在你指定的时间、用你指定的方式、处理你指定的数据、再把结果塞进你指定的字段里”。

为什么必须学它?因为现实中的 AI 项目,95% 的工作量根本不在模型本身。我在给一家制造业客户做设备故障问答系统时,花 3 天时间调通了 Llama 3 的本地推理;但接下来花了 11 天——整整两周——才搞定三件事:把他们分散在 SAP、MES 和 PDF 手册里的设备参数表统一清洗成向量;让模型在回答时能自动识别用户提问里的设备编号,并精准关联到对应产线;最后把生成的答案格式化成 JSON,直接推送到他们的工单系统 API。这三件事,LangChain 提供了现成的轮子: DocumentLoader 解析各种格式、 RetrievalQA 做上下文注入、 PydanticOutputParser 强制结构化输出。没有它,我得自己写爬虫、写向量化脚本、写正则提取编号、写 HTTP 客户端——而这些,和“大语言模型”本身毫无关系。

它解决的是工程落地的“最后一公里”问题。就像当年 jQuery 没有发明 JavaScript,但它让前端工程师不用再天天跟 IE6 的事件冒泡死磕;LangChain 也没发明大模型,但它把所有和“让模型在真实业务里干活”相关的脏活累活,打包成可复用、可调试、可监控的组件。你不需要成为大模型专家,但你必须懂怎么把它变成你业务流水线上的一个稳定工位。这就是为什么标题问“为啥要学它”,答案不是“因为它火”,而是“因为你迟早要把它焊进你的系统里,晚学一天,就多写一天重复代码”。

关键词已经非常清晰: LangChain、AI应用开发框架、标准化接口、模型集成、大语言模型 。这不是一个玩具库,而是一套面向生产环境的基础设施协议。下面我会用一个老手带新人的实际视角,一层层拆开它的骨架、血肉和神经,告诉你它到底在干什么、为什么这么干、以及你第一天上手时最该盯住哪几个关键点。

2. 核心设计哲学:不是造轮子,是定义轮子的尺寸和轴距

LangChain 的架构图看起来很复杂,一堆 Chain、Agent、Tool、Memory、Retriever 像乐高零件堆在一起。但如果你只记住一句话,就能瞬间看懂所有设计: LangChain 的一切,都是为了把“非结构化”的大模型能力,强行塞进“结构化”的软件工程范式里。 它不试图改变大模型的混沌本质,而是用一套精密的适配器,让混沌的输出能被程序可靠地消费。

2.1 为什么必须抽象“模型”?——告别硬编码的 API 调用

想象一下,你今天用 OpenAI 的 GPT-4 Turbo,明天客户要求切到本地部署的 Qwen2-72B,后天又得对接阿里云的百炼平台。如果每换一个模型,你都要重写 requests.post() 的 URL、重调 json.dumps() 的参数、重改错误码处理逻辑……项目还没上线,你已经成了 API 配置工程师。

LangChain 的 Model 组件,就是干这个的。它把所有模型调用,抽象成两个核心动作: 输入一段文本,返回一段文本(或向量) 。你看它的基类定义:

class BaseLLM(BaseModel, ABC):
    @abstractmethod
    def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str:
        """Run the LLM on the given prompt and return the result."""
        pass

就这么简单。 _call 方法,输入 prompt 字符串,输出 str 字符串。至于背后是调 OpenAI 的 REST API、还是加载 HuggingFace 的 .bin 权重、还是转发请求到你自建的 vLLM 服务——对上层业务代码完全透明。你只需要初始化不同的子类:

# OpenAI
from langchain.llms import OpenAI
llm = OpenAI(model_name="gpt-4-turbo", temperature=0.3)

# 本地 Llama.cpp
from langchain.llms import LlamaCpp
llm = LlamaCpp(
    model_path="./models/llama-3-8b.Q4_K_M.gguf",
    n_ctx=4096,
    n_threads=8
)

# 阿里云百炼
from langchain.llms import Tongyi
llm = Tongyi(
    dashscope_api_key="sk-xxx",
    model_name="qwen-max"
)

所有这些 llm 实例,你都能用同一行代码调用:

response = llm("请总结以下会议纪要:...")

这背后的工程价值是什么? 是解耦。当你把模型当成一个黑盒函数来用,你的业务逻辑(比如“从合同里抽甲方名称”)就和底层技术栈彻底分离了。测试时你可以用便宜的 gpt-3.5-turbo 快速验证流程,上线时再无缝切到 gpt-4-turbo ;客户说要国产化,你只要换一个 llm 初始化,其他几百行代码动都不用动。这种抽象,不是炫技,是应对真实世界技术迭代的生存策略。

提示:别被 LLM ChatModel 两个类搞晕。 LLM 是纯文本补全(适合写文章、生成代码), ChatModel 是消息序列对话(适合聊天机器人)。它们的抽象层级相同,只是输入格式不同。选哪个,取决于你的任务场景,而不是模型本身强弱。

2.2 为什么必须封装“提示”?——把玄学变成可版本管理的配置

大模型工程师最常被吐槽的一句话是:“我调了 2 小时,就为了加一个句号,让模型不胡说八道。” 提示工程(Prompt Engineering)听起来很酷,实操起来就是一场和模型随机性的搏斗。而 LangChain 的 PromptTemplate ,就是把这场搏斗变成可复现、可协作、可上线的工程实践。

它做了三件关键事:

  1. 变量注入 :把动态内容(用户问题、数据库查询结果)和静态模板(系统指令、格式要求)彻底分开。
  2. 格式标准化 :强制所有提示都走 format() 流程,杜绝字符串拼接导致的引号错乱、空格丢失。
  3. 可复用性 :一个模板可以被多个链、多个 Agent 共享,修改一处,全局生效。

看一个真实案例。我们给银行做的反洗钱报告生成系统,需要模型从交易流水里识别可疑模式。原始提示可能是这样硬编码的:

# ❌ 危险!无法维护,无法测试
prompt = f"""你是一名资深反洗钱合规官。请严格按JSON格式输出,不要任何解释。
交易流水如下:
{transaction_data}
请分析是否存在以下可疑行为:1. 分散转入集中转出;2. 快进快出;3. 与客户身份明显不符。
输出格式:{{"suspicious": true/false, "reason": "简短说明", "pattern": "匹配的模式编号"}}"""

用 LangChain 改写:

# ✅ 工程化!可测试,可版本控制
from langchain.prompts import PromptTemplate

PROMPT_TEMPLATE = """
你是一名资深反洗钱合规官。请严格按JSON格式输出,不要任何解释。
交易流水如下:
{transaction_data}
请分析是否存在以下可疑行为:
1. 分散转入集中转出;
2. 快进快出;
3. 与客户身份明显不符。
输出格式:{{"suspicious": true/false, "reason": "简短说明", "pattern": "匹配的模式编号"}}
"""

prompt = PromptTemplate(
    template=PROMPT_TEMPLATE,
    input_variables=["transaction_data"]
)

# 使用时
formatted_prompt = prompt.format(transaction_data=raw_data)
response = llm(formatted_prompt)

现在,这个 PROMPT_TEMPLATE 字符串可以放进 prompts.yaml 配置文件,由合规部门审核后发布;可以写单元测试,固定 raw_data 输入,断言输出 JSON 的 suspicious 字段是否为 true ;甚至可以 A/B 测试,对比两个不同版本的提示模板在 1000 条样本上的准确率。 提示,从此不再是写在代码注释里的玄学,而是产品需求文档的一部分。

2.3 为什么必须强加“输出解析”?——让自由文本变成可编程的数据

这是新手最容易忽略、却最致命的一环。大模型的输出是自由文本,而你的业务系统需要的是结构化数据。你不能指望模型永远按你想要的格式输出。我见过太多项目,因为没做输出解析,在上线后疯狂报错:

  • 用户问“北京今天天气”,模型回答:“北京今天晴朗,最高气温25度,适合户外活动。” —— 你的前端想取 temperature 字段,结果得到 None
  • 合同审查系统要求输出 {"risk_level": "high/medium/low"} ,模型却写了“风险等级:高”。

LangChain 的 OutputParser 就是专门治这个病的。它不靠祈祷,而是靠“引导+校验+兜底”三板斧:

  1. 引导(Prompt Injection) :在提示里明确告诉模型你要什么格式。
  2. 校验(Schema Validation) :用 Pydantic 或 JSON Schema 定义合法输出结构。
  3. 兜底(Fallback) :当模型输出非法时,自动重试或返回默认值。

继续用上面的反洗钱例子:

from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

class AMLReport(BaseModel):
    suspicious: bool = Field(description="是否可疑")
    reason: str = Field(description="简短说明,不超过20字")
    pattern: int = Field(description="匹配的模式编号,1/2/3")

parser = PydanticOutputParser(pydantic_object=AMLReport)

# 把解析规则注入提示
format_instructions = parser.get_format_instructions()
# PROMPT_TEMPLATE += f"\n{format_instructions}" # 追加到模板末尾

# 调用模型后,强制解析
try:
    parsed_result = parser.parse(response)
    # parsed_result 是一个真正的 AMLReport 对象,有 .suspicious 属性
    if parsed_result.suspicious:
        send_alert_to_compliance_team(parsed_result)
except Exception as e:
    # 解析失败,记录日志,触发人工审核流程
    log_error_and_fallback(raw_data, response)

这一步的价值,是把 AI 的不确定性,关进了软件工程的确定性牢笼里。 你的下游系统,永远拿到的是一个 AMLReport 对象,而不是一串可能随时变化的字符串。这才是生产环境能接受的 AI。

3. 四大核心组件深度拆解:从“能用”到“用稳”的实战细节

LangChain 的文档喜欢讲概念,但真实项目里,你每天打交道的是这四个组件: Model PromptTemplate OutputParser Retriever 。它们不是并列关系,而是一个严密的数据流闭环。下面我用一个正在运行的客户项目——“智能采购询价助手”——来逐个拆解,告诉你每个组件在真实战场上的作用、陷阱和优化技巧。

3.1 Model:不只是“调 API”,而是“管生命周期”

在采购系统里, Model 组件承担着远超“发请求”的职责。它要处理:

  • 成本控制 :GPT-4 Turbo 每千 token 0.01 美元,而 gpt-3.5-turbo 只要 0.0015 美元。一个询价请求平均 500 tokens,差价就是 4.25 美元/次。我们必须能根据询价金额、供应商等级,动态选择模型。
  • 降级策略 :当 OpenAI API 延迟超过 2 秒,自动切到本地 Qwen2-7B,保证页面不卡死。
  • Token 管控 :采购询价单包含大量表格数据,必须预估输入长度,超长时自动摘要,避免 context length exceeded 错误。

实现方案不是写一堆 if-else ,而是利用 LangChain 的 BaseLLM 扩展机制:

from langchain.llms import OpenAI, LlamaCpp
from langchain.callbacks.manager import CallbackManager
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler

class SmartProcurementLLM(BaseLLM):
    """采购专用LLM,内置成本、延迟、降级策略"""
    
    openai_llm: OpenAI = Field(default=None)
    local_llm: LlamaCpp = Field(default=None)
    cost_threshold_usd: float = Field(default=0.005)  # 单次询价成本阈值
    
    def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str:
        # 1. 预估OpenAI成本
        estimated_tokens = len(prompt) // 4  # 粗略估算
        estimated_cost = estimated_tokens * 0.01 / 1000
        
        # 2. 成本低于阈值,且延迟OK,用OpenAI
        if (estimated_cost < self.cost_threshold_usd and 
            self._is_openai_fast()):
            return self.openai_llm._call(prompt, stop)
        
        # 3. 否则降级到本地模型
        return self.local_llm._call(prompt, stop)
    
    def _is_openai_fast(self) -> bool:
        # 实际项目中,这里会调用Prometheus监控API获取SLA
        return True  # 简化示意

# 初始化时注入两个模型实例
llm = SmartProcurementLLM(
    openai_llm=OpenAI(model_name="gpt-4-turbo"),
    local_llm=LlamaCpp(model_path="./qwen2-7b.Q4_K_M.gguf")
)

实操心得 :别把 Model 当成一个静态对象。在生产环境,它必须是一个有心跳、有血压、能自主决策的“智能代理”。我建议所有项目都从第一天起,就给 Model 加上 CallbackManager ,记录每次调用的 input_tokens output_tokens total_time model_name 。这些数据,是你后续做成本优化、性能调优的唯一依据。没有监控的 LLM,就像没有刹车的汽车。

3.2 PromptTemplate:从“写提示”到“编排提示工作流”

采购询价助手的提示,绝不是一句“请分析以下询价单”。它是一个多阶段工作流:

  1. 信息抽取阶段 :从 PDF 或 Excel 询价单里,精准识别物料编码、数量、规格、交期。
  2. 知识检索阶段 :拿着物料编码,去公司知识库查历史成交价、合格供应商列表、技术协议要点。
  3. 决策生成阶段 :综合以上信息,生成推荐供应商、预估价格区间、标注风险点。

LangChain 的 PromptTemplate 本身只负责第一阶段。但它的真正威力,在于和 Chain Retriever 的组合。我们用 SequentialChain 编排整个流程:

from langchain.chains import SequentialChain
from langchain.chains import LLMChain

# 阶段1:信息抽取链
extract_prompt = PromptTemplate(
    template="你是一个采购数据分析师。请从以下询价单文本中,严格提取JSON:{text}\n输出:",
    input_variables=["text"]
)
extract_chain = LLMChain(llm=llm, prompt=extract_prompt, output_key="extracted_data")

# 阶段2:知识检索(Retriever已预先构建好)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# 阶段3:决策生成链(使用检索到的上下文)
decision_prompt = PromptTemplate(
    template="基于以下历史数据:{context}\n和本次询价需求:{extracted_data}\n请生成JSON格式的采购建议:",
    input_variables=["context", "extracted_data"]
)
decision_chain = LLMChain(llm=llm, prompt=decision_prompt, output_key="procurement_advice")

# 组合成工作流
overall_chain = SequentialChain(
    chains=[extract_chain, decision_chain],
    input_variables=["text"],
    output_variables=["procurement_advice"],
    verbose=True  # 关键!开启verbose,能看到每一步的输入输出
)

注意事项 SequentialChain verbose=True 是调试神器。它会在控制台打印出每一步的 input output ,让你一眼看出是“信息没抽出来”,还是“知识库没检索到”,还是“模型在决策阶段胡说八道”。没有这个,你就是在黑暗中调试。

提示: PromptTemplate partial_variables 参数常被低估。它可以预设一些不变的系统指令,比如 "你必须用中文回答,且不能编造信息" 。这样,所有使用该模板的链,都天然带上这个约束,避免在每个地方重复写。

3.3 OutputParser:结构化输出的“保险丝”与“转换器”

采购建议的最终输出,必须是下游 ERP 系统能直接消费的 JSON。但模型偶尔会失控。我们的 OutputParser 不仅要做解析,还要做三件事:

  1. 保险丝(Circuit Breaker) :当解析连续失败 3 次,自动触发人工审核流程,而不是让错误数据流入 ERP。
  2. 转换器(Transformer) :把模型输出的 "price_range": "¥500-¥800" ,转换成 ERP 要求的 {"min_price": 500.0, "max_price": 800.0}
  3. 审计追踪(Audit Trail) :记录原始模型输出和最终解析结果,用于事后追责。

我们用自定义 OutputParser 实现:

from langchain.output_parsers import BaseOutputParser
import json
import re

class ProcurementOutputParser(BaseOutputParser):
    """采购专用解析器,带保险丝和转换"""
    
    max_retries: int = Field(default=3)
    retry_count: int = Field(default=0)
    
    def parse(self, text: str) -> dict:
        try:
            # 1. 尝试标准JSON解析
            data = json.loads(text.strip())
            
            # 2. 转换 price_range 字段
            if "price_range" in data:
                match = re.search(r'¥(\d+)-¥(\d+)', data["price_range"])
                if match:
                    data["min_price"] = float(match.group(1))
                    data["max_price"] = float(match.group(2))
                    del data["price_range"]
            
            # 3. 添加审计字段
            data["parsed_at"] = datetime.now().isoformat()
            data["original_output"] = text
            
            return data
            
        except json.JSONDecodeError as e:
            self.retry_count += 1
            if self.retry_count >= self.max_retries:
                # 触发人工审核
                raise ValueError(f"Parse failed {self.max_retries} times. Original: {text}")
            # 返回一个空的、但结构合法的默认值,让流程继续
            return {"suggested_supplier": "", "min_price": 0.0, "max_price": 0.0, "risk_notes": ["解析失败,已人工介入"]}
    
    def get_format_instructions(self) -> str:
        return "请严格按JSON格式输出,包含suggested_supplier, min_price, max_price, risk_notes字段。"

parser = ProcurementOutputParser()

常见问题 :新手常犯的错误是,把 OutputParser 当成万能胶水,试图用正则硬扒模型输出。这极其脆弱。正确做法是: PydanticOutputParser + get_format_instructions() 强力引导模型输出规范格式,再用自定义解析器做兜底和转换。 引导是主攻,兜底是保险。

3.4 Retriever:不是“搜索”,而是“构建可信的知识边界”

在采购系统里, Retriever 是最核心的组件。它决定了模型“知道什么”。我们不用通用搜索引擎,而是构建了一个三层知识索引:

索引层 数据源 用途 更新频率
L1 - 实时询价池 当日新收询价单 保证模型能“看到”最新需求 实时
L2 - 历史成交库 过去3年ERP成交记录 提供价格基准、供应商表现 每日增量
L3 - 技术协议库 PDF/Word技术协议、质检标准 确保规格描述准确 按需

Retriever 的实现,不是简单调 Chroma.similarity_search() ,而是 MultiVectorRetriever + 自定义 retriever

from langchain.retrievers import MultiVectorRetriever
from langchain.storage import InMemoryStore
from langchain.embeddings import OpenAIEmbeddings

# 为每一层知识构建独立的向量存储
l1_store = Chroma(embedding_function=embeddings, persist_directory="./l1")
l2_store = Chroma(embedding_function=embeddings, persist_directory="./l2")
l3_store = Chroma(embedding_function=embeddings, persist_directory="./l3")

# 构建多层检索器
retriever = MultiVectorRetriever(
    vectorstore=l1_store,  # 主检索器
    docstore=InMemoryStore(),  # 存储原始文档
    search_kwargs={"k": 2}  # 每层最多取2个
)

# 自定义检索逻辑:根据询价单特征,动态加权各层
def custom_retrieve(query: str) -> List[Document]:
    # 分析query,判断是价格询价(重L2)、还是技术询价(重L3)
    if "价格" in query or "cost" in query.lower():
        # 从L2历史库取更多结果
        l2_results = l2_store.similarity_search(query, k=3)
        l1_results = l1_store.similarity_search(query, k=1)
        return l2_results + l1_results
    else:
        # 技术相关,重L3
        l3_results = l3_store.similarity_search(query, k=3)
        return l3_results

实操心得 Retriever 的质量,直接决定整个应用的可信度。我坚持一个原则: 绝不让模型“自由发挥”它不知道的事。 所有 Retriever search_kwargs 都要设置 k=1 k=2 ,宁可漏掉,也不要召回无关噪声。在采购这种高风险场景,模型说“我不知道”,远比它瞎编一个价格要安全一万倍。

4. 从零搭建一个生产级 RAG 系统:手把手拆解每一步

光讲理论没用。现在,我带你完整走一遍,如何用 LangChain 搭建一个“手把手搭建个人知识库 RAG 系统”的最小可行产品(MVP)。这个系统要能:上传 PDF/Word,自动切片、向量化、存储;接收自然语言提问,返回带来源引用的答案。整个过程,我会暴露所有关键决策点、参数计算依据和避坑经验。

4.1 环境准备:虚拟环境与依赖的精确控制

别跳过这一步。我见过太多人因为 Python 版本、依赖冲突,在第一步就卡死三天。

我的推荐配置(2025年实测稳定):

  • Python 3.11.9 (不是最新的 3.12,因为很多底层库如 llama-cpp-python 还未完全适配)
  • pip install --upgrade pip setuptools wheel (先升级包管理器)
  • 核心依赖:
    pip install langchain==0.2.11 \
        langchain-community==0.2.10 \
        langchain-openai==0.1.22 \
        chromadb==0.4.24 \
        pypdf==4.2.0 \
        python-docx==1.1.2 \
        unstructured==0.10.30 \
        tiktoken==0.7.0
    

为什么是这些版本? langchain==0.2.11 是当前最稳定的 0.2.x 版本,修复了 0.1.x 中 RetrievalQA 的内存泄漏; chromadb==0.4.24 修复了 Windows 下的文件锁问题; unstructured==0.10.30 是最后一个支持 pdfminer (处理扫描版 PDF)的版本。这些都不是随便选的,是踩过坑后记在笔记里的。

注意: unstructured 依赖 libmagic ,在 macOS 上用 brew install libmagic ,在 Ubuntu 上用 apt-get install libmagic1 。Windows 用户直接装 unstructured 会失败,必须先装 chocolatey choco install libmagic

4.2 文档加载与切片:不是“分段”,而是“理解语义边界”

很多人以为 TextSplitter 就是按字数切。错。切片质量,直接决定 RAG 效果。我们用 RecursiveCharacterTextSplitter ,但参数必须精心计算:

from langchain.text_splitter import RecursiveCharacterTextSplitter

# 关键参数计算逻辑:
# - chunk_size: 模型 context window 的 1/3。GPT-4 Turbo 是 128K tokens,所以取 ~40K。
#   但实际文档是中文,1个汉字≈2tokens,所以 chunk_size 设为 20000 字符(约10K tokens)。
# - chunk_overlap: 保证语义连贯。设为 chunk_size 的 15%,即 3000 字符。
# - separators: 按中文习惯,优先按 \n\n(段落)、\n(换行)、。、!、?(句号)切。
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=20000,
    chunk_overlap=3000,
    separators=["\n\n", "\n", "。", "!", "?", ";", ",", " "],
    keep_separator=True,
    strip_whitespace=True
)

实操步骤:

  1. 加载文档 :用 PyPDFLoader (PDF)、 Docx2txtLoader (Word)、 UnstructuredFileLoader (通用)。
  2. 预处理 :删除页眉页脚、OCR 文字纠错(对扫描 PDF)、统一中英文标点。
  3. 切片 :调用 text_splitter.split_documents(documents)
  4. 后处理 :过滤掉纯空白、纯数字、少于 50 字的碎片。

避坑经验 unstructured 加载 PDF 时,默认会保留图片和表格的占位符(如 <image> <table> )。这对 RAG 是灾难——模型会去“理解”这些占位符。必须在加载时禁用:

loader = UnstructuredPDFLoader(
    file_path="manual.pdf",
    mode="elements",  # 不要 "single" 模式
    strategy="fast",  # 不要 "hi_res"(会保留图像)
    post_processors=[lambda x: re.sub(r'<[^>]+>', '', x)]  # 清除所有HTML标签
)

4.3 向量化与存储:ChromaDB 的生产级配置

ChromaDB 是最轻量、最适合个人知识库的向量数据库。但默认配置是玩具级的。生产级配置如下:

import chromadb
from chromadb.config import Settings

# 生产级配置
client = chromadb.PersistentClient(
    path="./chroma_db",  # 指定持久化路径,不是内存
    settings=Settings(
        anonymized_telemetry=False,  # 关闭遥测
        allow_reset=True,  # 允许重置(调试用)
        is_persistent=True,
    )
)

# 创建集合,指定 embedding function
collection = client.create_collection(
    name="knowledge_base",
    embedding_function=OpenAIEmbeddings(
        model="text-embedding-3-small",  # 小模型,快且便宜
        dimensions=512,  # 指定维度,和后续查询一致
    ),
    metadata={"hnsw:space": "cosine"}  # 指定相似度算法
)

# 批量添加文档(不是单条add,效率差10倍)
collection.add(
    documents=[chunk.page_content for chunk in chunks],
    metadatas=[chunk.metadata for chunk in chunks],
    ids=[f"doc_{i}" for i in range(len(chunks))],  # 必须唯一
)

参数详解:

  • dimensions=512 text-embedding-3-small 的输出是 512 维向量。如果这里写错,插入和查询会失败。
  • hnsw:space="cosine" :HNSW 索引的相似度算法。 cosine 最常用, l2 (欧氏距离)适合数值型。
  • batch_size collection.add() 默认批量,无需手动分批。

性能对比(实测):

方式 1000 个 chunk 插入时间 内存占用
单条 add() 42 秒 1.2 GB
批量 add() 3.8 秒 320 MB

4.4 RAG 链构建: RetrievalQA 的深度定制

RetrievalQA 是 LangChain 为 RAG 封装的最简链。但“最简”不等于“最好用”。我们必须深度定制:

from langchain.chains import RetrievalQA
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, SystemMessage

# 1. 构建高级提示模板(带系统指令和上下文注入)
qa_system_prompt = """你是一个专业的知识库助手。请基于以下提供的上下文片段,准确、简洁地回答用户问题。
- 如果上下文中有明确答案,请直接给出,不要额外解释。
- 如果上下文不足以回答,请说“根据现有知识库,无法回答该问题”。
- 答案中必须包含所引用的上下文来源(如:来源:《XX手册》第3章)。
- 用中文回答。

<context>
{context}
</context>"""

qa_prompt = ChatPromptTemplate.from_messages([
    SystemMessage(content=qa_system_prompt),
    MessagesPlaceholder(variable_name="chat_history"),  # 为记忆留位置
    HumanMessage(content="{question}")
])

# 2. 创建检索器(带重排序)
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

# 用交叉编码器重排序,提升 top-k 准确率
compressor = CrossEncoderReranker(
    model=HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base"),
    top_n=3  # 重排序后只取前3个
)
retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=collection.as_retriever(search_kwargs={"k": 10})  # 先取10个粗筛
)

# 3. 构建最终 QA 链
qa_chain = RetrievalQA.from_chain_type(
    llm=OpenAI(model_name="gpt-4-turbo"),
    chain_type="stuff",  # 最简单,把所有context拼一起喂给模型
    retriever=retriever,
    chain_type_kwargs={
        "prompt": qa_prompt,
        "verbose": True  # 再次强调,调试必开
    }
)

# 4. 执行查询(带记忆)
result = qa_chain.invoke({
    "question": "如何更换设备的主控板?",
    "chat_history": []  # 初始为空
})
print(result["result"])

关键点解析:

  • chain_type="stuff" :适用于小知识库(<1000 个 chunk)。如果知识库很大,用 "refine" "map_reduce" ,但会慢很多。
  • ContextualCompressionRetriever :先用向量检索粗筛 10 个,再用更准但更慢的交叉编码器重排序,取前 3 个。这是精度和速度的黄金平衡点。
  • MessagesPlaceholder :为后续加入 ConversationBufferMemory 留下接口,现在先空着。

4.5 部署为 Web 服务:FastAPI + LangChain 的最佳实践

一个不能被调用的 RAG,等于没有。我们用 FastAPI 封装:

from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse
import uvicorn
import os

app = FastAPI(title="Personal Knowledge Base API")

@app.post("/upload")
async def upload_document(file: UploadFile = File(...)):
    """上传文档并自动处理"""
    try:
        # 保存文件
        file_path = f"./uploads/{file.filename}"
        with open(file_path, "wb
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐