《AI大模型应用开发实战从入门到精通共60篇》008、LangChain框架入门:构建LLM应用的第一块积木
LangChain框架入门:构建LLM应用的第一块积木
上周帮团队排查一个生产事故,日志里躺着一行诡异的报错:openai.error.InvalidRequestError: Engine not found。同事盯着代码看了半小时,死活想不通——明明昨天还能跑通的对话机器人,今天怎么就找不到引擎了?我扫了一眼他的代码,发现他直接调用了OpenAI的原始API,把模型名写死在engine="text-davinci-003"里。问题出在OpenAI去年底就把这个模型退役了,而他的代码里没有任何模型切换的抽象层。
这就是典型的“裸调LLM”后遗症。当你手头只有一个API调用时,换模型、加记忆、做链式调用,每一步都得改核心代码。LangChain就是来解决这个问题的——它不生产大模型,只是大模型的“适配器”和“胶水”。
从一次痛苦的Prompt调试说起
先看一个我早期踩过的坑。当时想做一个文档摘要工具,代码长这样:
importopenaidefsummarize(text):response=openai.ChatCompletion.create(model="gpt-3.5-turbo",messages=[{"role":"system","content":"你是一个文档摘要助手"},{"role":"user","content":f"请总结以下内容:{text}"}])returnresponse.choices[0].message.content这段代码的问题不是跑不通,而是改不动。产品经理说“能不能加个输出格式要求”,你得改prompt;说“换个模型试试”,你得改参数;说“把历史对话也传进去”,你得重写消息构造逻辑。两周后,这个函数膨胀到80行,里面塞满了if-else判断模型类型。
LangChain的解法很直接:把LLM调用拆成“模型”、“提示模板”、“输出解析器”三个独立组件。模型只管发请求,模板只管拼字符串,解析器只管格式化结果。各干各的,互不干扰。
安装与第一个Chain
别急着pip install,先确认Python版本。LangChain 0.1.x开始要求Python 3.8.1以上,我见过有人在3.7环境里装了个旧版,结果langchain.chains模块都导不进去。建议直接用3.10。
pipinstalllangchain langchain-openai注意这里分开装了两个包。langchain是核心框架,langchain-openai是OpenAI的集成包。别这样写:
# 别这样写!这是旧版用法,新版已废弃fromlangchain.llmsimportOpenAI新版应该这样:
fromlangchain_openaiimportChatOpenAI# 这里踩过坑:环境变量OPENAI_API_KEY没设的话,会直接抛认证错误llm=ChatOpenAI(model="gpt-4",temperature=0.7)ChatOpenAI返回的是一个LLM对象,但它还不能直接干活。你需要把它塞进一个Chain里。最简单的Chain叫LLMChain:
fromlangchain.promptsimportChatPromptTemplatefromlangchain.chainsimportLLMChain prompt=ChatPromptTemplate.from_messages([("system","你是一个{role},请用{style}风格回答"),("human","{input}")])chain=LLMChain(llm=llm,prompt=prompt)result=chain.run(role="技术顾问",style="简洁",input="什么是LangChain?")print(result)看到没?{role}、{style}、{input}这些占位符,在运行时才填充。这就是模板化的威力——改prompt不用改代码,改参数就行。
别被“链”这个词吓到
很多人第一次接触LangChain,看到“链”就以为是链表或者责任链模式。其实LangChain的Chain就是一个可调用对象,输入一个字典,输出一个字典。LLMChain的输入是prompt变量,输出是{"text": "..."}。
但真正让Chain强大的是组合能力。比如我想先让LLM生成一个文章大纲,再根据大纲写全文。传统写法要调两次API,中间还得手动传参。用LangChain的SequentialChain:
fromlangchain.chainsimportSequentialChain# 第一个链:生成大纲outline_prompt=ChatPromptTemplate.from_messages([("human","请为'{topic}'写一个详细的大纲,包含3个主要章节")])chain1=LLMChain(llm=llm,prompt=outline_prompt,output_key="outline")# 第二个链:根据大纲写文章article_prompt=ChatPromptTemplate.from_messages([("human","根据以下大纲写一篇完整的文章:\n{outline}")])chain2=LLMChain(llm=llm,prompt=article_prompt,output_key="article")# 串联起来full_chain=SequentialChain(chains=[chain1,chain2],input_variables=["topic"],output_variables=["article"])result=full_chain({"topic":"AI大模型应用开发"})print(result["article"])这里有个容易翻车的地方:output_key必须和下一个链的输入变量名一致。我见过有人把chain1的output_key写成"outline_text",但chain2的prompt里用的是{outline},结果运行时直接报KeyError。调试这种问题最烦人,因为LangChain的错误信息有时候不够明确。
Memory:让LLM记住你是谁
LLM本身是无状态的,每次调用都是独立的。但很多应用需要上下文,比如聊天机器人要记住之前说过什么。LangChain提供了多种Memory实现。
最简单的ConversationBufferMemory:
fromlangchain.memoryimportConversationBufferMemoryfromlangchain.chainsimportConversationChain memory=ConversationBufferMemory()conversation=ConversationChain(llm=llm,memory=memory)conversation.predict(input="你好,我叫张三")conversation.predict(input="你还记得我叫什么吗?")第二次调用时,LLM会回答“你叫张三”。因为Memory把历史对话拼到了prompt里。但这里有个坑:默认的ConversationBufferMemory会把所有历史都塞进去,对话长了token会爆。生产环境一定要用ConversationSummaryMemory或者ConversationTokenBufferMemory,前者用LLM总结历史,后者按token数截断。
我踩过最深的坑是Memory的return_messages参数。默认是False,返回的是字符串拼接的历史。如果你用Chat模型,最好设成True,返回消息列表,这样模型能更好理解对话结构。
memory=ConversationBufferMemory(return_messages=True)实战:一个带记忆的客服机器人
把上面这些拼起来,写一个能记住用户信息的客服机器人:
fromlangchain_openaiimportChatOpenAIfromlangchain.promptsimportChatPromptTemplate,MessagesPlaceholderfromlangchain.memoryimportConversationSummaryMemoryfromlangchain.chainsimportLLMChain llm=ChatOpenAI(model="gpt-4",temperature=0.3)prompt=ChatPromptTemplate.from_messages([("system","你是一个电商客服,态度友好,回答简洁"),MessagesPlaceholder(variable_name="history"),("human","{input}")])memory=ConversationSummaryMemory(llm=llm,return_messages=True,memory_key="history"# 这里踩过坑:必须和MessagesPlaceholder的variable_name一致)chain=LLMChain(llm=llm,prompt=prompt,memory=memory)# 模拟对话chain.run(input="我上周买的手机屏幕碎了,能保修吗?")chain.run(input="订单号是JD20231234")chain.run(input="我什么时候能收到换货?")注意MessagesPlaceholder这个组件。它告诉LangChain:“这里要插入历史消息”。如果不加这个,Memory里的历史不会被拼进prompt,机器人就会失忆。
关于模型选择的个人建议
LangChain支持几十种模型,但别贪多。我个人的经验是:
- 原型阶段:用
ChatOpenAI的gpt-3.5-turbo,速度快,成本低 - 生产环境:如果预算够,上gpt-4;如果追求性价比,试试Claude或者本地部署的Qwen
- 千万别:在同一个Chain里混用不同厂商的模型,除非你清楚知道每个模型的输出格式差异
另外,temperature这个参数我建议设成0.3以下用于事实性任务(如摘要、提取),0.7以上用于创意性任务(如写作、头脑风暴)。设成0.0虽然确定性高,但会让模型变得死板,连同义词替换都不愿意做。
调试技巧:打开详细日志
LangChain的调试信息默认是关闭的。当你遇到“模型返回了空字符串”或者“Chain执行顺序不对”时,可以这样打开:
importlangchain langchain.debug=True这会打印出每一步的输入输出,包括prompt最终长什么样、模型返回了什么、Memory里存了什么。我调试Memory问题时,全靠这个发现memory_key配错了。
还有一个更细粒度的方式,设置环境变量:
exportLANGCHAIN_VERBOSE=true最后说几句
LangChain不是银弹。它解决的是“LLM应用开发中的重复劳动”,而不是“LLM本身的能力问题”。如果你的prompt写得稀烂,换什么框架都没用。我见过有人用LangChain搭了个复杂的Agent,结果LLM连基本的工具调用都做不对——问题不在框架,在模型选型。
入门阶段,先掌握LLMChain、PromptTemplate、Memory这三个核心概念就够了。别一上来就研究Agent、Tool、Retriever,那些是后面的事。先把积木搭稳,再考虑盖高楼。
下一期我会写LangChain的进阶玩法:如何用OutputParser把模型输出转成结构化数据,以及如何用Runnable接口替代老旧的Chain API。这两个东西,能让你少写一半的异常处理代码。
