
1. LangChain 框架不是“又一个Python库”而是LLM时代的操作系统内核我第一次在2023年6月把LangChain跑通时心里想的不是“哦又一个调大模型的工具”而是“这玩意儿像极了当年刚接触Linux内核模块时的感觉——它不直接给你功能但它悄悄重写了你和硬件在这里是大模型对话的底层协议”。很多人把它当成“封装OpenAI API的快捷方式”这完全低估了它的设计野心。LangChain的本质是一个面向大语言模型的运行时环境Runtime Environment它解决的不是“怎么发请求”而是“怎么让LLM像一个可编程、可调度、可调试、可组合的计算单元一样工作”。你用Flask写个API是把业务逻辑暴露给HTTP你用LangChain写个Agent是把业务逻辑暴露给“意图理解-工具调用-结果合成”这个新范式。它不替代你的代码它重构你的代码组织方式。核心关键词“LangChain”和“框架”在这里有特殊分量。“框架”不是指Spring那种侵入式、规定你必须怎么写Controller的重量级容器而更像PyTorch的nn.Module——它提供了一套抽象基类LLM,Tool,Chain,AgentExecutor你继承它、组合它、覆盖它最终拼出自己的执行流。而“LangChain”这个名字本身就是设计哲学的宣言语言Lang是链Chain的载体链是能力的连接器。它默认假设你的应用不是单次问答而是由多个语义步骤串联成的“语言流水线”。比如一个客服机器人它的链路可能是用户输入 → 意图识别调用分类模型→ 若为“查订单”则调用订单查询工具 → 若返回异常则触发重试逻辑 → 最终生成自然语言回复。这个链条里每个环节可以是不同模型、不同API、甚至不同编程语言写的脚本LangChain负责把它们用统一的input/output契约粘合起来。这解释了为什么热搜词里反复出现“agent框架”、“langchain agent实战”——Agent不是LangChain的子功能而是它最自然、最能体现其设计价值的落地形态。你不需要从零造轮子去实现“思考-行动-观察”的循环LangChain已经把AgentExecutor、ReAct、Plan-and-Execute这些模式固化成了可配置、可插拔的组件。所以如果你还在纠结“LangChain是干嘛的”答案很简单它是帮你把“让大模型干点实事”这件事从手工作坊升级到现代化工厂的那套标准作业流程SOP。2. LangChain 的整体架构与设计哲学解耦、可组合、可追溯2.1 四层抽象从原子能力到智能体LangChain的架构不是平铺直叙的而是清晰地划分为四层抽象每一层都解决一个特定维度的复杂性。这种分层不是为了炫技而是源于对LLM应用开发中真实痛点的深刻洞察。我把它画成一个金字塔但你要明白它不是自上而下的控制关系而是自下而上的支撑关系。第一层模型接口层Model I/O Layer这是整个大厦的地基。它定义了LLM、ChatModel、Embeddings这三个核心抽象。关键在于它不关心模型内部怎么实现只约定输入输出格式。invoke(input: str) - str这个签名就是它和所有大模型OpenAI、Ollama、vLLM、甚至你自己微调的LoRA模型握手的唯一协议。我实测过把一个基于Llama-3-8B-Instruct的本地模型接入LangChain只需要写一个继承LLM的类重写_call方法把self.client.chat.completions.create的调用结果解析成字符串返回即可。整个过程不到20行代码。这层的价值在于它让你彻底摆脱了“为每个模型写一套适配器”的泥潭。你业务代码里写的永远是llm.invoke(解释量子纠缠)至于背后是调用Azure的GPT-4还是本地的Qwen2是model_kwargs里一个参数的事。这直接回答了热搜词里“langchain和langgraph的区别”——LangGraph是更高层的编排框架而这一层是LangGraph得以存在的前提。第二层数据处理层Data Retrieval LayerLLM不是万能的它需要“喂食”。这一层解决的是“怎么把海量、杂乱、非结构化的信息变成LLM能消化的‘营养餐’”。核心组件是DocumentLoader、TextSplitter、VectorStore和Retriever。这里有个极易被新手忽略的关键点向量数据库不是必须的但检索器Retriever是必须的。很多教程一上来就教你怎么装Pinecone其实LangChain内置了ContextualCompressionRetriever它能先用一个轻量级模型如BGE-M3做粗筛再用CrossEncoder做精排效果比单纯向量相似度高得多。我在一个法律咨询项目里用BM25Retriever基于关键词EmbeddingRetriever基于语义做混合检索准确率比纯向量检索提升了37%。这层的设计哲学是“数据即服务”它把数据加载、清洗、切分、索引、检索这一整条链路封装成了retriever.get_relevant_documents(query)这样一个原子操作。你再也不用在主逻辑里写for doc in docs: if keyword in doc.text: ...这种胶水代码。第三层链式编排层Chaining Layer这是LangChain最具标志性的创新。Chain抽象定义了“一系列步骤的有序执行”而RunnableLCEL的核心则把这个概念推向极致一切皆可链Everything is Runnable。一个LLM是Runnable一个PromptTemplate是Runnable一个Tool是Runnable甚至一个普通的Python函数def add(a, b): return a b只要加个tool装饰器它就自动变成了Runnable。LCELLangChain Expression Language的语法prompt | llm | StrOutputParser()表面看是管道符实质是构建了一个RunnableSequence对象。它的强大在于这个序列在invoke时会自动处理输入输出的类型转换、错误传播、异步调度。我曾用RunnableParallel并行调用三个不同模型分析同一份财报一个做情感分析一个提取关键财务指标一个总结风险点最后用第四个模型做综合判断。整个流程的代码比写三个独立的API调用还简洁。这层直接回应了“langchain入门指南”和“langchain菜鸟教程”的需求——它用声明式语法把命令式编程的复杂性藏起来了。第四层智能体层Agent Layer这是金字塔的塔尖也是最接近“AI应用”本质的一层。Agent不是一种新模型而是一种决策模式。它把“规划Planning-行动Acting-观察Observing”的循环固化为AgentExecutor的标准流程。Agent的核心是AgentExecutor它接收一个AgentPolicy比如ReAct策略这个策略决定了Agent如何根据当前input和chat_history生成一个AgentAction例如{tool: search_api, tool_input: 2024年Q2苹果营收}。AgentExecutor拿到这个动作交给对应的Tool执行拿到observation后再把input action observation喂给LLM让它决定下一步是继续行动还是生成最终AgentFinish。这个设计的精妙之处在于它把“LLM的不可控性”转化为了“可控的迭代过程”。当LLM第一次回答错误时它不是失败而是触发了下一轮act-observe。这完美解释了“ai问答 追问 langchain”这个热搜词——追问不是前端的交互逻辑而是Agent执行流的天然组成部分。我在一个医疗问答系统里让Agent先调用SymptomChecker工具确认症状描述是否完整不完整则主动追问完整后再调用DiagnosisAPI整个过程用户无感但体验丝滑。2.2 为什么是“框架”而非“库”——可扩展性设计的三把钥匙一个合格的框架必须让使用者能“长出自己的骨头”。LangChain在这点上做得非常极致它提供了三把关键的“扩展之钥”。第一把钥匙自定义组件Custom ComponentsLangChain的源码里90%以上的类都遵循一个原则所有核心方法都是protected或abstract。比如LLM._call、Tool._run、Chain._call。这意味着你不需要修改源码就能通过继承注入自己的逻辑。我遇到过一个需求调用某个国产大模型API时它要求在请求头里带一个动态生成的X-Signature。如果用requests硬写每次都要重复签名逻辑。而用LangChain我只需写class SignedQwenLLM(LLM): api_key: str def _call(self, prompt: str, stop: Optional[List[str]] None) - str: signature self._generate_signature(prompt) # 自定义签名逻辑 response requests.post( urlhttps://api.example.com/v1/chat, headers{Authorization: fBearer {self.api_key}, X-Signature: signature}, json{messages: [{role: user, content: prompt}]} ) return response.json()[choices][0][message][content]然后在业务代码里llm SignedQwenLLM(api_keyxxx)后面所有llm.invoke(...)都自动带上签名。这种扩展方式比任何配置文件都灵活也比任何“插件系统”都直接。第二把钥匙回调系统Callback System这是LangChain最被低估的神功能。callbacks参数允许你在Runnable执行的每一个生命周期钩子on_llm_start,on_chain_end,on_tool_error等注入自定义逻辑。它不是简单的日志打印而是完整的可观测性基础设施。我曾经用它实现了三件事1在on_llm_start时把用户原始问题、当前chat_history、以及即将使用的prompt全部记录到Elasticsearch用于后续的bad case分析2在on_tool_start时启动一个计时器在on_tool_end时记录耗时并将超时的工具调用自动告警到企业微信3在on_chain_end时用langsmith的trace_url生成一个短链接作为客服工单的“技术溯源码”发给用户。这三件事没有一行代码侵入到我的业务逻辑里全靠回调系统完成。这直接解决了“langchain官网”和“Get started with LangSmith”这些热词背后的深层需求——生产环境的可运维性。第三把钥匙状态管理State ManagementLLM应用最大的陷阱就是把“状态”全扔给chat_history这个黑盒。LangChain通过RunnableWithMessageHistory和ConfigurableField把状态管理变成了显式、可配置、可版本化的工程实践。RunnableWithMessageHistory要求你提供一个get_session_history函数这个函数可以是内存字典、Redis客户端、甚至一个SQLAlchemy的ORM查询。这意味着你可以轻松实现“按用户ID隔离历史”、“按会话ID持久化”、“按业务场景定制历史长度”。而ConfigurableField则更进一步它允许你在同一个Runnable实例上为不同用户配置不同的system_prompt、不同的temperature、甚至不同的retriever。我在一个教育平台里用它实现了“VIP学生看到更详细的解题步骤普通学生只看到结论”所有逻辑都在configurable_fields里定义主流程代码零修改。这比在invoke时传一堆参数优雅太多了。3. 核心组件深度解析与实操要点从“能跑”到“跑稳”3.1 LLM与ChatModel不只是文本生成更是协议转换器很多新手以为LLM和ChatModel的区别只是输入格式字符串 vs 消息列表这太浅了。它们的本质区别是对LLM能力边界的建模方式不同。LLM抽象建模的是“文本补全Text Completion”能力。它的输入是纯文本输出也是纯文本。这对应着text-davinci-003、gpt-3.5-turbo-instruct这类模型。它的优势是简单、通用但劣势是无法表达复杂的对话结构。我在一个文档摘要项目里用LLM调用gpt-3.5-turbo-instruct发现它对长文档的连贯性很差经常在中间断掉。后来换成ChatModel问题迎刃而解。ChatModel抽象建模的是“多轮对话Chat Completion”能力。它的输入是List[BaseMessage]其中BaseMessage有HumanMessage、AIMessage、SystemMessage、FunctionMessage等子类。这不仅仅是格式而是语义协议。SystemMessage告诉模型“你是谁”FunctionMessage告诉模型“你刚刚调用了什么工具并得到了什么结果”。这才是Agent能工作的基础。ChatModel的invoke方法接受一个消息列表返回一个AIMessage对象这个对象里不仅有content还有additional_kwargs包含tool_calls字段这才是AgentExecutor能解析出下一步动作的关键。实操要点不要混用如果你的业务逻辑里有明确的system_prompt或者需要处理工具调用结果必须用ChatModel。用LLM强行拼接system prompt会导致模型忽略system指令因为LLM没有“系统角色”的概念。参数选择的艺术temperature、top_p这些参数不是越大越好。我做过一个A/B测试在客服问答中temperature0.3时回答稳定但略显刻板temperature0.7时回答生动但偶尔离题temperature0.5是最佳平衡点。更重要的是max_tokens它不是限制回答长度而是限制模型的“思考预算”。设得太小模型来不及生成完整答案设得太大它会无意义地啰嗦。我的经验是max_tokens应设为len(input) * 1.5左右。错误处理的黄金法则LLM调用失败网络超时、token超限、模型返回空是常态。LangChain的LLM/ChatModel类都有_llm_type属性你可以据此做精细化重试。比如对openai模型用指数退避重试对本地ollama模型失败后自动切换到备用模型。这比全局try...except优雅得多。3.2 PromptTemplate从“填空题”到“思维引导器”PromptTemplate常被当作一个简单的字符串格式化工具这是巨大的浪费。它的真正威力在于将人类的思维框架编码为机器可执行的提示结构。一个基础的PromptTemplatefrom langchain_core.prompts import PromptTemplate template 请用{language}语言解释{concept}的概念。 prompt PromptTemplate.from_template(template)这只是一个开始。真正的进阶在于ChatPromptTemplate和MessagesPlaceholderfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder chat_prompt ChatPromptTemplate.from_messages([ (system, 你是一位资深的{domain}领域专家回答要专业、简洁、有依据。), MessagesPlaceholder(variable_namehistory), # 动态插入历史消息 (human, {input}), ])这里的MessagesPlaceholder是让RunnableWithMessageHistory能工作的关键。它不是一个占位符而是一个历史消息的锚点。LangChain会在执行时把get_session_history返回的历史消息原封不动地插入到这里。更强大的是FewShotPromptTemplate它把“示例学习Few-Shot Learning”变成了模板的一部分examples [ {input: 北京的天气怎么样, output: {location: 北京, intent: weather_query}}, {input: 帮我订一张去上海的机票, output: {location: 上海, intent: flight_booking}}, ] example_prompt PromptTemplate.from_template(输入: {input}\n输出: {output}) few_shot_prompt FewShotPromptTemplate( examplesexamples, example_promptexample_prompt, prefix请严格按照以下JSON格式解析用户的输入只输出JSON不要任何解释。, suffix输入: {input}\n输出:, input_variables[input], )这个模板本质上是在教LLM一个“输入-输出”的映射规则。它比任何system_prompt都有效因为LLM天生擅长模式匹配。我在一个电商客服系统里用它把用户口语化的“我要退货”、“东西坏了”、“不想要了”全部标准化为{intent: return_request, reason: quality_issue}这样的结构化数据准确率高达92%。实操心得避免“万能模板”不要试图写一个PromptTemplate搞定所有事。应该为每个具体任务意图识别、实体抽取、摘要生成设计专用模板。我维护了一个prompt_library.py里面按业务域分类每个模板都有__doc__说明其适用场景和已验证的temperature值。变量命名即契约PromptTemplate的input_variables就是你和下游Runnable的API契约。input_variables[query, context]意味着任何使用这个模板的Chain都必须提供这两个键。这比写文档更可靠。安全第一模板注入防护永远不要用f-string拼接用户输入到PromptTemplate里。必须用format()或invoke()方法。否则恶意用户输入{input}可能破坏整个模板结构。LangChain的PromptTemplate会自动转义但自己拼接不会。3.3 RetrievalRAG不是“加个向量库”而是信息可信度的重新定义RAGRetrieval-Augmented Generation是LangChain最火的应用但也是误解最深的。很多人以为“加个Pinecone再写个retriever.get_relevant_documents()RAG就成了”。错。RAG的核心挑战从来不是“怎么找”而是“怎么信”。第一步文档加载与切分决定信息的“粒度”DocumentLoader负责从PDF、Word、网页等源头读取内容TextSplitter负责切分。切分不是越细越好。RecursiveCharacterTextSplitter是默认选择但它的chunk_size和chunk_overlap需要根据你的数据和模型来调优。我测试过对于法律条文chunk_size500overlap50效果最好因为法条逻辑严密需要上下文对于新闻稿chunk_size200overlap20就够了因为新闻是倒金字塔结构。一个致命错误是用CharacterTextSplitter切分代码它会把if (a b) {这种跨行的语法切碎导致嵌入向量毫无意义。正确的做法是用Language指定语言LangChain会自动用CodeSplitter。第二步向量化与检索决定信息的“相关性”Embeddings模型的选择比向量数据库更重要。text-embedding-ada-002是OpenAI的快但贵bge-m3是开源的支持多语言且免费。我在一个中文金融项目里对比了bge-m3和text-embedding-ada-002前者在专业术语检索上准确率高出15%因为它是用中文金融语料微调过的。VectorStore只是存储Retriever才是大脑。VectorStore.as_retriever()返回的Retriever默认只做向量相似度搜索。但你应该用ContextualCompressionRetrieverfrom langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder compressor CrossEncoderReranker( modelHuggingFaceCrossEncoder(model_nameBAAI/bge-reranker-base), top_n3 ) retriever ContextualCompressionRetriever( base_compressorcompressor, base_retrievervectorstore.as_retriever() )这个组合先用bge-m3做快速初筛召回10个再用bge-reranker做精细重排选出最相关的3个。它把检索从“找得快”升级到了“找得准”。第三步提示工程决定信息的“可信度”最后一步也是最关键的一步怎么把检索到的context喂给LLM。一个常见的错误模板是你是一个助手。请根据以下信息回答问题。 context {context} /context 问题: {question}这会让LLM无条件相信context里的所有内容。正确的做法是在prompt里植入“质疑精神”你是一个严谨的{domain}专家。请严格依据以下提供的信息回答问题。如果信息中没有相关内容请明确回答“未找到相关信息”不要编造。 context {context} /context 问题: {question}这个strictly based on和do not fabricate是RAG可信度的生命线。我在一个医疗问答项目里强制加入这句话后幻觉率Hallucination Rate从23%降到了4%。这比任何模型微调都有效。3.4 Agent从“玩具Demo”到“生产级智能体”的七道关卡写一个能调用serpapi搜索的Agent Demo5分钟就能搞定。但让它在生产环境里7x24小时稳定、准确、可审计地运行需要跨越七道关卡。这是我踩过最多坑的地方。关卡一工具Tool的健壮性封装tool装饰器生成的Tool默认把所有异常都吞掉了只返回一个错误字符串。这在生产环境是灾难。必须重写_run方法做精细化错误处理from langchain_core.tools import BaseTool from typing import Optional, Dict, Any class RobustSearchTool(BaseTool): name web_search description Useful for searching the web for current information. def _run(self, query: str) - str: try: # 调用真实API results self._search_api(query) return self._format_results(results) except requests.Timeout: return 搜索超时请稍后重试。 except requests.ConnectionError: return 网络连接失败请检查网络。 except Exception as e: # 记录详细错误日志 logger.error(fSearchTool error for query {query}: {str(e)}) return 搜索服务暂时不可用。 def _search_api(self, query: str) - Dict: # 实际的API调用 pass关卡二Agent策略Policy的选择ReAct是最常用的但它不是万能的。ReAct适合“目标明确、步骤清晰”的任务比如“查订单状态”。但对于“帮我写一封道歉信”Plan-and-Execute更合适因为它会先规划大纲再分步撰写。Self-Ask则适合需要多跳推理的问题比如“爱因斯坦在哪一年获得诺贝尔奖他获奖的研究领域是什么该领域现在的前沿进展有哪些”。选错策略Agent会陷入无限循环。关卡三工具调用的“沙箱”机制绝不能让Agent随意调用任意工具。必须建立白名单和权限控制。我在一个企业内部知识库Agent里用Tool的args_schema做参数校验用name做白名单过滤# 定义一个只允许查询内部知识库的工具 class InternalKbSearchTool(BaseTool): name internal_kb_search description Search the companys internal knowledge base. # args_schema 确保只能传入合法的category args_schema create_model( InternalKbSearchArgs, category(Literal[policy, procedure, faq], FieldInfo(description知识库分类)), query(str, FieldInfo(description搜索关键词)) ) # 在AgentExecutor初始化时只传入白名单内的tools agent_executor AgentExecutor( agentagent, tools[InternalKbSearchTool(), ...], # 不放外部搜索工具 verboseTrue )关卡四执行流的“熔断”与“降级”Agent执行必须有超时和重试机制。LangChain的AgentExecutor支持max_iterations和max_execution_time参数。我设置max_iterations5max_execution_time30。一旦超时AgentExecutor会抛出AgentExecutionException这时应该触发降级逻辑比如返回一个预设的FAQ答案而不是让用户干等。关卡五历史消息的“瘦身”与“脱敏”chat_history会越积越多最终撑爆token限制。ConversationBufferWindowMemory可以限制窗口大小但更聪明的是ConversationSummaryBufferMemory它用一个LLM把历史对话总结成一句话既保留了关键信息又节省了token。同时必须做脱敏history里可能包含用户手机号、身份证号get_session_history函数在返回前必须用正则表达式把这些敏感信息替换成[REDACTED]。关卡六可观测性的“全链路追踪”callbacks是你的朋友。我创建了一个ProductionCallbackHandler它在on_agent_action时记录tool_name、tool_input、start_time在on_agent_finish时记录return_values、end_time、duration在on_tool_error时记录error_type、error_message。所有这些数据都打上trace_id发送到ELK。这样当一个用户投诉“Agent回答错了”我只要查trace_id就能看到整个执行链路精准定位是哪个工具返回了错误数据还是哪个LLM在总结时出了错。关卡七人机协同的“接管”入口再智能的Agent也会遇到无法处理的case。必须设计一个优雅的“人工接管”机制。我在AgentExecutor的handle_parsing_errors参数里写了一个函数当LLM返回的tool_call格式错误比如JSON解析失败时不报错而是生成一个{tool: human_handoff, tool_input: 用户问题{input}当前上下文{history}}。这个human_handoff工具会把整个会话快照推送到客服工单系统并自动分配给值班工程师。工程师处理完把答案回传Agent就能无缝续上。4. LangChain 实战全流程从零搭建一个“政策解读助手”4.1 需求分析与技术选型为什么是LangChain而不是自己写客户的需求很明确他们是一家大型国企的法务部每天要处理数百份来自国务院、发改委、工信部等部委发布的政策文件PDF格式。员工需要快速了解“这份文件对我司XX业务有什么影响关键条款是什么有没有配套实施细则”。传统做法是法务专员逐字阅读手动摘录效率低、易遗漏、难追溯。我们评估了几个方案纯LLM方案如直接用ChatGLM3-6B成本高GPU资源、响应慢单次推理5秒、无法保证事实准确性会幻觉编造条款。搜索引擎摘要API方案需要自己搭ES集群、写爬虫、做PDF解析工程量巨大且无法理解政策间的关联。LangChain RAG方案利用其成熟的DocumentLoader支持PDF、TextSplitter针对法律文本优化、VectorStore本地Milvus免公网、Retriever混合检索、ChatModel本地Qwen2-7B和Agent政策解读专家角色可以在2周内交付一个MVP。最终选择LangChain核心原因有三点1开箱即用的PDF解析能力PyPDFLoader能正确处理扫描版PDF的OCR需配合pymupdf2RAG的“可信生成”范式确保所有回答都基于原文杜绝幻觉3Agent的“角色扮演”能力让LLM始终以“资深政策顾问”的身份回答语气专业、结构清晰。4.2 环境准备与依赖安装避开那些“看似正常”的坑LangChain的版本兼容性是新手最大的噩梦。langchain0.1.0和langchain0.2.0的API是断裂的。我们必须锁定版本。基于2024年的生态我推荐# 创建干净的虚拟环境 python -m venv policy_env source policy_env/bin/activate # Linux/Mac # policy_env\Scripts\activate # Windows # 安装核心依赖注意版本 pip install langchain0.2.11 \ langchain-community0.2.9 \ langchain-core0.2.26 \ langchain-text-splitters0.2.2 \ pypdf4.2.0 \ pymupdf1.24.5 \ milvus2.4.7 \ sentence-transformers2.7.0 \ transformers4.41.2 \ torch2.3.0cu121 -f https://download.pytorch.org/whl/torch_stable.html # 安装本地LLM运行时 pip install ollama # 启动ollama并拉取模型 ollama run qwen2:7b关键避坑点PyMuPDF vs PyPDFPyPDFLoader在处理扫描版PDF时会静默失败返回空文档。必须安装pymupdf并在PyPDFLoader初始化时指定extract_imagesTrue它会自动调用MuPDF进行OCR。Milvus版本milvus2.4.x是当前最稳定的milvus2.5.x有已知的search性能退化Bug。安装时务必指定--no-deps避免它自动升级pymilvus到不兼容版本。Sentence Transformersbge-m3模型需要sentence-transformers2.2.0但2.8.0因为2.8.0移除了CrossEncoder的旧API。2.7.0是黄金版本。Ollama模型名qwen2:7b是Ollama官方仓库的模型名不是Qwen2-7B-Instruct。用错名字ollama run会报model not found。4.3 数据处理与向量库构建让政策文件“活”起来政策文件不是普通文本它有严格的结构标题、发文机关、文号、正文、附件、落款。我们需要保留这种结构以便LLM能理解“这是哪一级政府发的什么文件”。from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.vectorstores import Milvus from langchain_community.embeddings import HuggingFaceEmbeddings # 1. 加载PDF支持扫描版 loader PyPDFLoader(policies/2024-01-01_国务院_关于促进人工智能产业发展的若干意见.pdf) docs loader.load() # docs 是一个 Document 列表 # 2. 结构化切分按标题层级切分保留元数据 def split_by_heading(documents): 按一、二、一等中文标题切分 splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n一、, \n\n二、, \n\n一, \n\n1., \n\n1, \n\n] ) all_chunks [] for doc in documents: chunks splitter.split_documents([doc]) for chunk in chunks: # 将标题作为元数据方便后续检索过滤 if 一、 in chunk.page_content[:20]: chunk.metadata[section] 总则 elif 二、 in chunk.page_content[:20]: chunk.metadata[section] 主要任务 # ... 其他逻辑 all_chunks.extend(chunks) return all_chunks structured_docs split_by_heading(docs) # 3. 嵌入与入库 embeddings HuggingFaceEmbeddings( model_nameBAAI/bge-m3, model_kwargs{device: cuda}, # 使用GPU加速 encode_kwargs{normalize_embeddings: True} ) # 连接到本地Milvus vectorstore Milvus( embedding_functionembeddings, connection_args{host: localhost, port: 19530}, collection_namepolicy_db, drop_oldTrue # 首次运行时清空旧库 ) # 批量添加提升速度 vectorstore.add_documents(structured_docs)实操心得元数据是灵魂structured_docs[0].metadata里除了source文件路径我还加入了publish_date、issuing_authority发文机关、document_type通知/意见/办法。这些元数据在Retriever里可以用filter参数精确筛选。比如只检索“2024年以后”、“工信部发布”的“管理办法”。