LangChain核心概念,Chain、Agent、Tool、Memory、Retriever一文搞懂
LangChain核心概念,Chain、Agent、Tool、Memory、Retriever一文搞懂
记忆篇结束,我们进入LangChain深度篇。
前面三十多篇里,我们手写了记忆系统,手写了工具调用,手写了RAG流程。代码能跑,功能也有,但每写一个新功能都要从零搭一遍。提示词拼来拼去,消息列表手动维护,工具分发自己写if-else。功能越加越多,代码就越乱。
这种时候你会想,有没有人把这些常见套路封装好。LangChain就是干这个的。它把AI应用里反复出现的模式抽象成几个核心概念,Chain、Agent、Tool、Memory、Retriever。理解了这五个,LangChain的大部分代码你都能看懂。
市面上AI应用框架不少,LangChain、LlamaIndex、Semantic Kernel各有各的定位。LangChain生态最全,社区最大,坑也踩得最多,文档和教程遍地都是。我们这个专栏以它为主线,搞懂它的概念体系,换到别的框架也触类旁通。
这篇把这五个概念一次讲清楚,每个都配一段能跑的代码。
Chain,把步骤串起来
Chain解决的问题是,怎么把提示词、模型、输出解析这几步连成一条流水线。
最早LangChain的Chain是一堆继承BaseChain的类,LLMChain、SequentialChain之类。写法繁琐,调试也麻烦。后来官方推了LCEL,全称LangChain表达式语言。核心思路是用管道符把组件串起来,像Unix管道一样,前一步的输出自动喂给下一步。
fromlangchain_openaiimportChatOpenAIfromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParser model=ChatOpenAI(model="gpt-4o-mini",temperature=0)prompt=ChatPromptTemplate.from_template("用一句话向小学生解释什么是{concept}")chain=prompt|model|StrOutputParser()print(chain.invoke({"concept":"向量数据库"}))这段代码里,prompt负责把变量填进模板,model负责生成回答,StrOutputParser负责把AIMessage里的文本抠出来。三个组件用竖线连起来,数据从左往右流。
LCEL的好处是组合性强,你想在中间加一步翻译,直接再接一个管道就行。踩坑点也有,调试时如果某一步报错,报错信息有时不太直观,得自己把链条断开逐步排查。
Agent,让模型自己决定下一步
Chain是固定流程,你写好了A到B到C,它就老老实实走。但有些任务没法预先定死步骤。用户问"北京明天天气怎么样",模型得先决定调天气工具,拿到结果再组织回答。这种需要模型自己决策的场景,就用Agent。
Agent和Chain最大的区别在于控制权。Chain的流程是你定的,Agent的流程是模型定的。模型根据当前情况决定要不要调工具、调哪个工具、调几次。
fromlangchain_openaiimportChatOpenAIfromlangchain_core.toolsimporttoolfromlangchain.agentsimportcreate_tool_calling_agent,AgentExecutorfromlangchain_core.promptsimportChatPromptTemplate@tooldefadd(a:int,b:int)->int:"""两个整数相加"""returna+b@tooldefmultiply(a:int,b:int)->int:"""两个整数相乘"""returna*b model=ChatOpenAI(model="gpt-4o-mini")tools=[add,multiply]prompt=ChatPromptTemplate.from_messages([("system","你是一个计算助手,用工具完成计算"),("human","{input}"),("placeholder","{agent_scratchpad}"),])agent=create_tool_calling_agent(model,tools,prompt)executor=AgentExecutor(agent=agent,tools=tools,verbose=True)print(executor.invoke({"input":"3乘以5再加10"})["output"])这里模型会自己拆解,先算3乘5得15,再算15加10得25。你不用告诉它先乘后加,它自己判断。verbose参数打开后能看到完整的思考过程,调试时很有用,正式上线建议关掉省token。
Agent有个常见的坑是死循环。模型调了一次工具觉得结果不对,再调一次还是不满意,来来回回调个没完。AgentExecutor有个max_iterations参数能兜底,超过次数就强制停。生产环境一定要设这个参数,不然一个请求烧光你的token额度。
Tool,给模型装上手脚
Tool就是Agent能调用的函数。模型本身只能输出文字,有了Tool它才能操作外部世界。查天气、搜网页、算数学、查数据库,都可以包成Tool。
LangChain里定义Tool最简单的方式是用@tool装饰器。写一个普通函数,加上类型注解和docstring,装饰器会自动帮你转成Tool对象。模型靠docstring判断什么时候该用这个工具,所以docstring一定要写清楚,描述模糊了模型就会乱调。
fromlangchain_core.toolsimporttool@tooldefsearch_weather(city:str)->str:"""查询指定城市的实时天气情况"""weather_db={"北京":"晴 26度","上海":"多云 30度"}returnweather_db.get(city,f"暂无{city}的天气数据")# 单独调用print(search_weather.invoke({"city":"北京"}))# 也可以交给Agent,让模型自己决定什么时候调一个坑是返回值类型。模型拿到工具返回的内容后会基于它生成回答,返回值最好直接给人类可读的文本。返回复杂对象模型容易理解错。另一个坑是参数命名,用中文名还是英文名都行,但一定要和docstring描述对得上,不然模型容易传错参数。
Memory,让对话有连续性
大模型本身是无状态的,每次调用都像第一次见面。你要让它记住前面聊过什么,得自己维护消息历史。这就是Memory干的事。
老版本LangChain有ConversationBufferMemory这类封装类,现在官方更推荐直接管理消息列表,或者用LangGraph做状态管理。思路都一样,把历史消息攒起来,每次调用模型时一起传过去。
fromlangchain_openaiimportChatOpenAIfromlangchain_core.messagesimportSystemMessage,HumanMessage,AIMessage model=ChatOpenAI(model="gpt-4o-mini",temperature=0)history=[SystemMessage(content="你是一个友好的聊天助手")]defchat(user_input):history.append(HumanMessage(content=user_input))response=model.invoke(history)history.append(AIMessage(content=response.content))returnresponse.contentprint(chat("我叫老王,是个后端程序员"))print(chat("我刚才说我叫什么"))# 模型能答出"老王",因为历史消息都在history里消息列表会越来越长,token消耗跟着涨。实际项目里要做截断或摘要,只保留最近几轮,或者把早期对话压缩成摘要。这些技巧记忆篇讲过,这里就不重复了。
手动管理消息列表看着原始,好处是透明,出了问题你能看到每一条消息长什么样。等到业务复杂了再上LangGraph,它能把状态管理做得更细。
Retriever,从知识库里捞东西
Retriever解决的是知识获取问题。模型训练时学过的知识有截止日期,遇到私有数据或最新信息就抓瞎。Retriever先从知识库里检索相关内容,再塞给模型参考。
Retriever通常搭配向量数据库用。文档先切片、做embedding存进去,查询时把问题也变成向量,算相似度找最相关的几段。
fromlangchain_community.vectorstoresimportChromafromlangchain_openaiimportOpenAIEmbeddings embeddings=OpenAIEmbeddings()store=Chroma(embedding_function=embeddings)store.add_texts(["LangChain用LCEL来组合组件,用管道符连接","向量数据库把文本转成向量后按相似度检索","Agent能自主决定调用哪些工具完成任务",])retriever=store.as_retriever(search_kwargs={"k":2})docs=retriever.invoke("怎么让模型自己调工具")fordocindocs:print(doc.page_content)retriever返回的是Document列表,每段内容带元数据。检索质量好不好,跟切片策略、embedding模型、是否加rerank都有关系,RAG篇已经详细讲过。
五个概念怎么配合
这五个概念各有分工,组合起来才是一个完整的Agent应用。
Retriever负责从知识库找资料,把找到的内容交给模型参考。Tool负责连接外部能力,查天气查数据库都靠它。Memory负责记住对话历史,让交互有连续性。Agent负责统筹,根据用户输入决定要不要调工具、要不要检索、怎么组织回答。Chain负责把上面的组件按一定顺序串起来执行。
一个典型的流程是这样的。用户提问,Agent判断要不要查知识库,要的话让Retriever去检索。检索结果和对话历史一起塞进提示词。如果还需要外部数据,Agent调Tool去取。最后Chain把这些步骤串成一条管线,输出回答。
搞清楚这五个概念之后,你会发现LangChain的文档读起来顺畅很多。它所有的组件都在围绕这五个东西打转,花样再多也跑不出这个圈。
下一篇我们具体动手,用LCEL写几种常见的链式调用,把提示词、模型、解析器、检索器串成实战可用的流水线。
