资讯动态

LangChain编排框架实战:从裸调LLM到工程化AI应用

发布时间:2026/10/25 9:16:07 来源:尧图企业网站定制
实话说我第一次把一个大模型接进业务系统的时候心里是有点飘的——几千行业务逻辑好像只要塞给模型一个Prompt就能全部自动化。结果上线第一周就接连翻车用户连续问了几句我把整段历史原样拼进Prompt重新发给模型Token开销肉眼可见地涨模型答应返回JSON结果偶尔还夹带两句“好的我已经为您记录了”这种废话解析直接崩用户问“你们门店今天几点营业”模型一本正经地给了我一个编出来的时间。那时候我才彻底明白LLM再强也只是一台聪明的“文本生成引擎”它解决不了应用层的工程问题。纯粹靠反复调用模型接口就像请了一位知识渊博但没有工具、没有流程的实习生你得自己给他配工位、配流程、配抽屉。LangChain这类编排框架干的就是这件事。这篇文章是LangChain进阶系列的开篇我不打算直接甩API文档而是先聊清楚一个核心问题为什么我们必须引入编排框架它到底解决了裸调模型时哪些真实痛点最后我会用一个小例子把“裸调LLM”和“LangChain编排”两种写法放在一起做对比看完你就能理解所谓的编排到底在编什么。1.1 一个典型的裸调翻车现场先还原一个我非常熟悉的场景。我做过一个本地生活客服助手需求很简单用户问餐厅营业时间、推荐菜品、查预订信息。最开始图省事我没上任何框架代码大概长这样import json import requests # 手动拼历史消息 history [] def chat_with_llm(user_input): history.append({role: user, content: user_input}) prompt f你是本地生活客服助手。 请结合以下对话历史回答问题 {history} 如果用户询问营业时间请用JSON格式返回 {{type: business_hours, content: ...}} 当前用户问题{user_input} # 调用模型接口 resp requests.post( https://your-llm-endpoint.example/v1/chat/completions, headers{Authorization: Bearer YOUR_API_KEY}, json{ model: your-model, messages: [{role: user, content: prompt}], }, ) data resp.json() content data[choices][0][message][content] return content这段代码的问题玩过的人心里都有数历史记录全塞进一个字符串对话超过几轮Token直接爆炸模型偶尔输出不合法JSONjson.loads当场崩溃想查门店数据库模型却只会“编一个答案”。更隐蔽的问题是整个流程只有“一问一答”一旦中间要插入查询工具、缓存、纠错逻辑你就会发现所有代码都堆在一个函数里越改越乱。这不是某个特定模型的毛病而是所有LLM共同的天花板——它们本质上是一个“无状态”的推理器输入一段文本输出一段文本仅此而已。应用需要的那些能力比如记忆、工具调用、格式约束、流程控制全得靠外层的工程代码补上。1.2 LLM应用的真实构成模型只占一小半这里我想引入一个自己的理解一个完整的LLM应用从来不是“模型Prompt”这么简单它其实由四块拼图构成任务目标你要让模型帮你完成什么具体业务比如回答FAQ、抽取信息、写文案。上下文管理模型需要哪些背景信息历史对话如何组织知识库内容如何检索。能力边界模型本身不会查数据库不会调API不会读本地文件这些能力要靠外部工具注入。容错与治理模型输出不稳定格式可能错、内容可能幻觉怎么兜底、怎么重试、怎么记录。裸调模型时这四块全部要自己手写而且每块都会随着业务复杂化而失控。LangChain真正做的是把这四块各自抽象成标准组件然后用一条明确的管道串起来。模型只是管道里的一个节点前后还有无数节点在替它“补位”。这也是为什么我说“LLM不是万能药”——它非常擅长单点的语言任务但一个可用的产品需要的是稳定的系统而不是聪明的单点。2.1 从“一问一答”到“流水线”编排框架的核心贡献是把“调用模型”这件事从一段裸函数变成一条流水线。顺着这个思路我建议所有初学者先忘掉LangChain里各种花哨的类名只记住一个词Runnable可运行单元。在LangChain的现代版本里任何组件都可以看作Runnable。一个Prompt模板是Runnable一个大模型是Runnable一个输出解析器是Runnable一个自定义的Python函数也可以包装成Runnable。这些Runnable能像管道一样用|符号串起来组合出你想要的流程。举个例子from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI prompt ChatPromptTemplate.from_messages([ (system, 你是本地生活助手请用中文回答。), (human, {question}), ]) llm ChatOpenAI(model你的模型名, temperature0.2) chain prompt | llm这条链路的含义是先由Prompt模板把用户输入包装成完整指令再交给模型生成回复。写起来非常直观而且更关键的是这条链路本身还是一个Runnable你可以在任意环节插入新的组件比如在模型后面接一个输出校验器或者在Prompt前面接一个知识库检索器。用流水线的思维去组织代码最大的好处是每个环节都可单独测试、单独替换、单独观察。裸调模型的代码想改一个中间环节往往要牵连全局而管道式结构拆装都在明面上。2.2 状态管理让模型从“失忆症”里醒过来LLM自己没有记忆。每次调用都是全新开始上一轮说了什么它完全不记得。没有编排框架的时候大家普遍都是“拼历史”把之前的对话一股脑塞进Prompt。这个方法能跑但很快就会撞上两个硬钉子上下文窗口有限历史塞满了新问题就装不下无关旧消息大量挤占空间模型对当前问题的注意力反而被稀释。LangChain对这个问题做了统一抽象用Memory组件管理历史消息并且提供好几种记忆策略按场景选择。比如最简单的ConversationBufferMemory就是原样保留所有消息而ConversationSummaryMemory会在历史过长时用模型自动生成对话摘要还有窗口类策略只保留最近N轮。这里要提醒一句Memory并不是“外挂记忆”它的底层还是把历史消息拼进Prompt只是帮你把“怎么拼、拼多少、拼之后怎么截断”这些重复逻辑标准化了。我用过之后最大的感受是省心是一方面更重要的是记忆策略可以和业务代码解耦——以后想从“保留最近5轮”改成“自动摘要”只需要换一个Memory实现不用改业务逻辑。2.3 工具调用给模型装上“手脚”模型在训练时用的是静态语料它不知道你数据库里现在有什么也不知道外部接口此刻返回什么。想让模型具备实时能力就必须允许它“调用工具”——查数据库、算数学、调天气接口。LangChain里这个能力集中体现在Tool和Agent的抽象上。Tool只是一个普通的Python函数你给它写清楚名字、描述、参数结构模型就能在回答过程中“决定”要不要调用它。Agent则是更上层的决策器它有点像模型和工具之间的调度员模型说“我想查一下这家店的评分”Agent就去调用评分查询工具把结果返给模型继续生成。这个机制的底层依赖是函数调用能力不过说句掏心窝的话初学者一开始其实不太需要急着上Agent。先把Tool的注册和调用跑通理解“模型输出一个意图框架执行一个函数结果再喂回给模型”这个闭环后面的Agent只是在这个闭环上叠加决策循环而已。3.1 看得见的结构Prompt模板标准化虽然很多人觉得Prompt很玄学但LangChain里的Prompt处理其实相当务实。ChatPromptTemplate把不同角色的消息分开管理每个消息都明确标记是系统指令、用户输入还是历史记录。这种结构化的好处在于模型对“角色”的理解会更稳定不同消息的边界不会揉在一起。我在裸调时代最常见的问题是把系统指令、用户问题、历史记录全拼成一大段字符串模型有时候分不清“哪里是要求哪里是对话内容”表现时好时坏。换成模板之后系统指令、历史、当前问题各自归位模型接收到的结构永远清晰一致。这里有个实战经验模板里变量缺失时LangChain会直接抛异常这其实是好事至少比裸拼字符串时悄悄漏掉变量强得多。另外模板内容可以从配置文件或外部加载你在工程上可以把Prompt当成“参数”而不是“代码”来管理。我对团队的建议是Prompt模板单独建目录写清楚版本号和生效时间改Prompt不需要动代码发布这对迭代速度的帮助非常明显。3.2 输出约束让模型学会“说人话”结构化输出是LangChain里价值很高的组件。业务系统不可能接受模型给你来一段自由散文往往需要它返回合法的JSON、符合某个接口的字段。with_structured_output结合一个Schema定义能让模型直接按结构返回省去手动解析JSON的脆弱环节。用法大概是这样你定义一个Pydantic模型或者直接给一个JSON SchemaLangChain会把Schema作为约束条件塞进模型调用过程并自动完成结果校验。from pydantic import BaseModel, Field class RestaurantRecommendation(BaseModel): name: str Field(description餐厅名称) dish: str Field(description推荐菜品) reason: str Field(description推荐理由) llm_with_structure llm.with_structured_output(RestaurantRecommendation) result llm_with_structure.invoke(推荐一家适合吃辣的地方) print(result.name) # 直接访问字段而不是手动解析字符串这个能力的背后是函数调用机制但使用者不需要关心底层实现。我实测下来最大的变化是之前模型夹带废话导致JSON解析失败的情况基本消失因为框架会在结构校验失败时自动重试或报错而不是让你面对一坨“看起来像JSON又不是JSON”的字符串干瞪眼。不过有件事必须泼冷水结构化输出对模型能力有一定要求弱模型对复杂Schema的遵循能力明显下降。所以定义Schema时尽量精简字段别求全字段越多越容易翻车。3.3 检索器把知识库“喂”给模型前的一道工序RAG检索增强生成是LangChain引以为傲的能力但Retriever这个组件本身的本质其实并不神秘先把文档切成小块做Embedding向量化构建索引用户提问时把问题也变成向量然后找最相似的文档块塞进Prompt作为上下文。如果不用LangChain这套流程要自己写切分规则、向量库选型、索引构建、相似度计算每一步都要自己调试。LangChain把Retriever抽象成标准接口切分有TextSplitter向量库有统一封装甚至可以做“只检索不生成”的独立调用这让我在接公司内部知识库时省了非常多精力。实话说Retriever最需要调的不是框架而是“检索什么内容、怎么切分、取多少块”。我踩过的一个坑是切片尺寸设得太小一条完整业务规则被拦腰截断模型拿到半截信息就开始推理答案自然跑偏。切片时必须考虑语义完整性规则类文档以条款为单位切问答类文档以问答对为单位切不要无脑按字数硬切。3.4 可观测性Debug成了最实用的功能很多人忽视这块但我觉得LangChain带来最大的工程红利其实是可观测性。裸调模型时你根本不知道模型内部经历了什么为什么返回这个答案中间有没有调用工具哪一步Prompt写得不对LangChain的回调和追踪机制可以记录每一步的输入输出、Token消耗、耗时甚至可以把整条链路的执行过程可视化。我在调试一个复杂的Agent时曾经因为模型意外调错工具而排查了很久后来打开追踪面板一眼就看到了模型在哪个节点生成了错误的工具调用参数——问题定位从“猜”变成了“看”。对团队而言可观测性还意味着可复盘。每个线上请求的完整链路都能留存用户说“为什么上次回答不对”你可以直接把当时的链路调出来看它到底走了哪些分支、哪些上下文被注入、最终模板长什么样。4.1 目标场景做一个“本地美食推荐助手”光说不练没什么意思。我重新用一个简单但完整的小案例把“裸调”和“LangChain编排”放在一起让你直观感受差别。假设我们要做一个本地美食推荐助手用户随口说“我想吃点辣的”助手需要推荐一家合适的餐厅并且记住用户的偏好下次再来还能结合上次的偏好给建议。这个场景很小但包含了多轮记忆、结构化输出、业务规则插入三个常见需求。裸调版本的问题很明显历史消息要自己拼偏好要自己存推荐结果要自己解析JSON任何一个环节出错都得自己兜。代码我刚才已经展示过雏形实际跑起来之后你会发现维护成本集中在好几个“隐形陷阱”里手拼的JSON格式错了、历史消息把Prompt撑爆、模型偶尔在JSON后面多输出一行提示文字导致解析失败。4.2 用LangChain重构先搭Prompt与模型管道重构版本我按LangChain的习惯把链路拆成三层。第一层是Prompt模板和模型第二层是历史记忆第三层是偏好存储与结构化输出。先看第一层from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field class RecommendResult(BaseModel): restaurant: str Field(description推荐的餐厅名称) dish: str Field(description推荐菜名) reason: str Field(description推荐理由) prompt ChatPromptTemplate.from_messages([ (system, 你是一位熟悉本地餐饮的美食推荐助手。结合用户偏好给出推荐。), MessagesPlaceholder(history), (user, {user_input}), ]) llm ChatOpenAI(model你的模型名, temperature0.4) structure_llm llm.with_structured_output(RecommendResult) # 先跑一遍无记忆版本验证推荐质量 chain_no_memory prompt | structure_llm result chain_no_memory.invoke({ history: [], user_input: 我想吃点辣的, }) print(result.restaurant, result.dish)注意几个细节。MessagesPlaceholder(history)在模板中预留了历史消息的“插槽”运行时才把记忆注入进去这比手动拼字符串干净一万倍。with_structured_output(RecommendResult)保证了返回结果一定是RecommendResult对象不管模型中间输出什么杂质框架都会拦下来。跑通之后接下来给管道加记忆。4.3 用LangChain重构挂载记忆模块LangChain的现代API里给链路加记忆的推荐做法是用RunnableWithMessageHistory它能自动把历史消息注入到MessagesPlaceholder声明的位置并且自动管理消息存储。from langchain_core.chat_history import BaseChatMessageHistory from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_community.chat_message_histories import ChatMessageHistory store {} def get_session_history(session_id: str) - BaseChatMessageHistory: if session_id not in store: store[session_id] ChatMessageHistory() return store[session_id] chain_with_history RunnableWithMessageHistory( chain_no_memory, get_session_history, input_messages_keyuser_input, history_messages_keyhistory, ) # 第一次对话 chain_with_history.invoke( {user_input: 我想吃点辣的最好是川菜}, config{configurable: {session_id: user-001}}, ) # 第二次对话模型应该记得用户口味偏好 chain_with_history.invoke( {user_input: 换成不辣但类似的推荐}, config{configurable: {session_id: user-001}}, )模板里history变量会自动替换成这个Session之前的所有消息完全不用自己维护messages数组。这个抽象在公司里非常实用因为线上往往是多用户并发每个Session对应一个用户历史消息存储只需要实现按session_id读写即可。我这里做了个内存版如果你要上生产把它换成数据库存储就行。4.4 用LangChain重构偏好记录与业务规则插入最后加上业务规则。用户说“推荐辣的”我们不能真靠模型自己记忆应该在一开始就把用户过往偏好存起来在Prompt里显式注入。这个环节适合用RunnableLambda做边界处理。from langchain_core.runnables import RunnableLambda def load_user_preference(inputs: dict) - dict: # 这里本该查询数据库我简化为一个字典模拟 user_id inputs.get(user_id, default) prefs_db { user-001: [川菜, 重辣], user-002: [粤菜, 清淡], } inputs[user_preference] prefs_db.get(user_id, ) return inputs def attach_preference(inputs: dict) - dict: # 把偏好信息塞进Prompt需要的内容字段 return { user_input: inputs[user_input], history: inputs.get(history, []), preference: f用户口味偏好:{inputs.get(user_preference, 未知)}, } prompt_with_pref ChatPromptTemplate.from_messages([ (system, 你是本地美食推荐助手。{preference}), MessagesPlaceholder(history), (user, {user_input}), ]) full_chain ( RunnableLambda(load_user_preference) | RunnableLambda(attach_preference) | prompt_with_pref | structure_llm ) result full_chain.invoke({ user_id: user-001, user_input: 今晚想吃点开胃的, })这里RunnableLambda相当于在管道里插入普通Python函数用来做“模型之外”的确定性工作。比如查偏好、验权限、写日志都可以以节点的形式挂在链路上。LangChain优雅的地方在于它没有强迫所有事情都让模型做而是允许你用最普通的代码处理确定性逻辑模型只负责语言生成那一段。整个重构完成后对比一下裸调版本你会发现几个明显变化历史记录不再需要手拼结构化输出不再需要脆弱的json.loads偏好注入变成了管道中的一个显式节点哪一步出错可以直接看链路日志。代码行数不一定会少很多但结构清晰度和可维护性完全不在一个量级。5.1 LangChain的真正适用场景先讲适用面。我自己的判断标准很简单只要你的应用需要“多轮状态”或“外部工具”编排框架基本就是刚需。典型场景包括客服对话机器人需要多轮记忆、查询订单库、调用退换货接口。知识库问答系统需要文档检索把RAG流程串起来而不是手动处理切分、向量化、召回。内容生产流水线比如“先做选题分析再生成大纲再产出初稿再检查事实”每步可以拆成独立节点。需要快速迭代Prompt和模型的应用模板与代码分离方便运营人员参与调优。LangChain在这些场景下的价值不是让模型变聪明而是让“围绕模型的工程系统”变可靠。它把那些琐碎重复的胶水代码沉淀成通用组件让团队把精力放在业务本身。5.2 LangChain的边界和并不美好的另一面不能光说好话。LangChain的缺点也很鲜明最核心的一条是版本演进太快。早期版本的API和现在差异很大网上的教程很多已经过期照着写经常报错。我第一次用的时候也经历过“昨天还能跑今天升级一个小版本就全红”的窘境。所以我的建议是生产项目一定要锁定版本别追新。学习时以当前稳定版官方文档为准不要迷信老旧博客。核心链路尽量用LangChain的标准抽象冷门功能或者版本不稳定的部分用RunnableLambda包自己写的代码更省心。另一个边界是性能。LangChain在链路中加了不少逻辑封装网络请求和对象转换都会带来额外开销。如果你的场景是超高并发、极低延迟的纯文本生成且不需要多轮状态那裸调API反而更可控。毕竟编排框架的价值在于工程组织而不是魔法加速。6.1 高频翻车点清单我把实际使用中遇到的高频问题整理成了一张速查表建议你直接收藏。症状常见原因解决方案模型输出解析失败Schema定义太复杂模型理解不了精简字段数量字段描述写得更明确上下文还是超出了窗口历史消息无限增长换用窗口记忆或摘要记忆主动截断检索结果相关性差文档切分不合理按语义单元切分别按固定字数硬切Prompt模板里变量没被替换传入键名和模板变量名不一致开启模板变量校验写单测覆盖Agent工具调用错误工具描述不清晰参数解释不足在Tool描述里写清楚使用条件和返回格式线上链路排查困难没开日志追踪开启回调与追踪保存每次请求链路结果时好时坏温度参数太高生成类任务降至0.2左右抽取类任务接近06.2 几个我总结的排查技巧如果链路跑通了但结果不对我的排查顺序是这样的先看Prompt模板最终渲染出来的文本。LangChain里可以用chain.get_graph()查看链路结构也可以用调试模式直接打印每一步的输入输出确认历史消息、偏好等变量确实被替换了。再看模型输出。如果结构化结果校验失败框架一般会抛出明确异常此时把原始模型输出拉出来看一眼十有八九是输出里混入了字节序标记或Markdown代码块。最后看下游业务。有时候问题不在模型而在下游消费方——比如你按新Schema输出了restaurant字段但下游还在读老字段name自然匹配不上。接口变更时全局搜一下字段引用算是我栽过最多次的坑。关于检索器的调试我再多说一句不要只看最终回答对不对先把Retriever单独拎出来测——给定一个query看它召回了哪些文档块。如果召回的内容本身就偏了后面接什么模型都救不回来。6.3 关于版本兼容的一个建议LangChain生态现在分了好几个包langchain-core管基础抽象langchain管编排langchain-community和langchain-openai等管集成。新手经常会遇到缺包报错我的建议是统一装当前稳定版pip install langchain langchain-core langchain-openai langchain-community然后再跑一遍刚才的代码示例。如果你发现某个类的导入路径变了直接去搜“类名官方文档”别在旧博客里浪费时间。这类框架的文档更新速度永远追不上代码变化。我个人在实际操作中的体会是编排框架从来不是“AI能力的放大器”它是“工程确定性的回收站”。模型负责生成框架负责把生成之外的一切杂乱事务接住。你迟早会发现真正让一个AI应用稳定的不是某个聪明的模型而是围绕它建立的那条清晰、可观测、可兜底的流水线。这个系列后面几篇我会继续往里走把记忆管理、RAG检索、Agent循环拆开讲透。下一篇先从Memory开始——别看它只是一个“历史消息存取”用不好照样能让你的应用变笨。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑