LangChain最佳实践,我踩过的10个坑和解决方案
用LangChain做项目快一年了,从一开始照着官方文档抄代码,到后来被各种隐坑折腾得半夜睡不着,也算攒下了一本血泪账。今天挑10个让我印象最深的坑出来聊一聊,希望能帮你少走点弯路。
坑一、版本升级直接炸裂
最早我用的还是0.1那套写法,from langchain.chat_models import ChatOpenAI,跑得好好的。某天手贱升到0.3,一运行满屏ImportError。
原因很简单,LangChain把模型相关的类拆到了独立包里,chat_models这个路径已经废弃。0.2之后LLMChain标记成deprecated,0.3动得更大。
解决办法是按新规范导入,ChatOpenAI走langchain_openai,Prompt走langchain_core.prompts。
fromlangchain_openaiimportChatOpenAIfromlangchain_core.promptsimportChatPromptTemplate llm=ChatOpenAI(model="gpt-4o-mini",temperature=0)prompt=ChatPromptTemplate.from_messages([("system","你是一个Python助手"),("human","{question}"),])chain=prompt|llm升级前先看一眼CHANGELOG,别像我一样直接pip install -U然后懵圈。我还建议把依赖版本锁死在requirements.txt里,团队里别人拉代码不会因为版本不一致跑出不一样的结果。0.1和0.3的差别大到能把你的项目重写一遍,这一点我有切肤之痛。
坑二、temperature乱设一通
我刚开始所有场景都默认temperature=0.7,结果做JSON抽取的时候模型开始发挥创造力,字段名都能给你改了。
temperature控制输出的随机性。数值越低越确定,越高越发散。代码生成、信息抽取、分类这种要稳定结果的活,老老实实设0。写文案、头脑风暴再往上调。
# 抽取/分类场景extract_llm=ChatOpenAI(model="gpt-4o-mini",temperature=0)# 创意场景creative_llm=ChatOpenAI(model="gpt-4o-mini",temperature=0.9)我还踩过一个隐形坑,同一个llm对象被多个chain复用,以为改了参数其实改的是同一个实例。要复用就传参新建,别图省事。
坑三、Prompt全靠字符串拼接
第一版代码里我的prompt全是f"请回答{question}",问号、引号、特殊字符一进来就乱套,还经常把用户输入直接塞进去,注入风险拉满。
正经做法是用ChatPromptTemplate,变量用花括号占位,框架帮你转义。
fromlangchain_core.promptsimportChatPromptTemplate prompt=ChatPromptTemplate.from_template("请总结以下内容,字数不超过{max_words}字。\n\n内容:{content}")formatted=prompt.invoke({"max_words":100,"content":user_input})复杂一点的prompt我建议单独存成文件或者常量,别散落在业务代码里,改起来要命。
坑四、错误处理全靠运气
有阵子我的chain跑着跑着就挂,要么API超时,要么限流429,直接抛异常整个流程断掉,前面算的全白费。
LangChain的底层invoke不会自动重试,你得自己包一层。生产环境务必加上重试和超时。
fromlangchain_core.runnablesimportRunnableLambdaimporttimedefsafe_invoke(chain,inputs,retries=3):foriinrange(retries):try:returnchain.invoke(inputs,config={"timeout":30})exceptExceptionase:ifi==retries-1:raisetime.sleep(2**i)with_config里也能塞max_concurrency和重试策略,比手写循环优雅,我后来都改用它。
坑五、Token超限静默截断
做长文档总结时,我把整篇文章塞进prompt,模型没报错,但返回的总结明显缺了后半段。查了半天才发现是上下文超了,模型默默把后面的内容丢了。
很多模型超长输入会截断而不报错。解决办法是先算token数,超了就分段处理或者用map-reduce。
fromlangchain_text_splittersimportRecursiveCharacterTextSplitter splitter=RecursiveCharacterTextSplitter(chunk_size=2000,chunk_overlap=200,separators=["\n\n","\n","。",""],)chunks=splitter.split_text(long_text)用tiktoken预先估算token也行,心里有数比事后抓瞎强。我后来养成的习惯是,凡是处理超过两千字的内容,一律先过一遍splitter,宁可多调几次模型也别赌它不会截断。
坑六、工具描述写得太随意
做Agent的时候,工具的description我一开始就写"搜索网页"四个字,结果模型根本不知道啥时候该用,要么乱调,要么死活不用。
工具描述就是模型看的使用说明,要写清楚这个工具干什么、什么时候用、输入是什么格式。描述写得越具体,Agent调用越准。
fromlangchain_core.toolsimporttool@tooldefsearch_web(query:str)->str:"""当需要查找最新资讯、事实性数据或问题超出知识范围时使用。 输入应为简洁的中文搜索关键词,不要包含问号。"""returnresults我后来养成习惯,工具名用动词,描述里写清触发条件和输入规范,调用量蹭蹭涨。
坑七、Agent陷入死循环
有个ReAct Agent跑着跑着就开始反复调用同一个工具,同样的输入调了十几遍还不收敛,token烧得我心疼。
死循环一般有几个诱因。工具返回的内容太模糊,模型觉得没解决就反复试。或者prompt里没限制最大迭代次数。
解决办法是给Agent加max_iterations,再让工具返回更结构化的结果。
fromlangchain.agentsimportcreate_react_agent,AgentExecutor agent=create_react_agent(llm,tools,prompt)executor=AgentExecutor(agent=agent,tools=tools,max_iterations=5,early_stopping_method="generate",handle_parsing_errors=True,)handle_parsing_errors=True这个参数救过我很多次,解析失败时不会直接崩,会给模型一次纠错机会。
坑八、输出解析一直翻车
我用PydanticOutputParser解析模型输出,模型偶尔会在JSON外面加一句"好的,结果是",解析直接挂。
模型的输出很难百分百干净,尤其是让它直接生成结构化数据时。OutputParser的get_format_instructions能把格式要求塞进prompt,但还不够保险。
更稳的做法是搭配with_structured_output,直接让模型按schema返回。
frompydanticimportBaseModel,FieldclassSummary(BaseModel):title:str=Field(description="标题")keywords:list[str]=Field(description="关键词")structured_llm=llm.with_structured_output(Summary)result=structured_llm.invoke("总结一下RAG的原理")# result直接是Summary对象这招比手写正则提取稳太多,我现在能结构化的全用它。
坑九、API Key硬编码进仓库
别笑,我真干过把api_key="sk-xxx"写进代码然后push到GitHub的事,第二天就收到OpenAI邮件说key泄露了。
环境变量必须走配置文件或者系统环境变量,LangChain默认就会读OPENAI_API_KEY。
importosfromdotenvimportload_dotenv load_dotenv()# 从.env文件加载llm=ChatOpenAI()# 自动读取OPENAI_API_KEY.env加进.gitignore,团队里用.env.example做模板,谁也别图省事把密钥写死。
坑十、调试全靠print
chain一长,中间出了问题根本不知道是哪一步挂的,我一开始只会到处加print,代码乱得没法看。
LangChain的Runnable天然支持流式调试,用全局verbose或者debug开关就能看到执行步骤。
fromlangchain.globalsimportset_debug,set_verbose set_verbose(True)# 打印chain的执行步骤set_debug(True)# 更详细的调试信息# 或者单步查看中间结果fromlangchain_core.runnablesimportRunnablePassthrough chain=({"context":retriever,"question":RunnablePassthrough()}|prompt|llm)正式上线前记得把debug关掉,日志量很大。我调试阶段开verbose定位问题够用了。
小结
这10个坑里,版本兼容和错误处理是那种不出事不知道、一出事要命的类型,temperature和工具描述则是效果好坏的分水岭。写LangChain应用,多花点时间在prompt和参数调校上,比堆功能实在得多。我自己最大的教训是太迷信官方示例,照着抄完就上线,出了问题才发现人家示例里省掉了重试、超时、token校验这些保命逻辑。
到这里LangChain本身的实践就聊得差不多了。但你会发现,真要做复杂的多步Agent流程,单纯靠Chain拼装还是有点吃力,状态管理、循环控制、人在回路这些需求处理起来挺别扭。下一篇文章我们进入LangGraph篇,看看它是怎么把Agent编排这件事做得更顺手的。