前四篇各讲一块:链、LCEL、RAG、Agent。这一篇把它们揉成一个能用的东西:一个问答 Agent,问它公司的事,它会去你的文档里查;问它实时数据,它会调接口;聊到第三句,它还记得第一句说的啥。
下面是一份最小可跑的完整代码,然后把几个上线前必补的点拆开说。
完整代码
from langchain_openai import ChatOpenAI from langchain_chroma import Chroma from langchain_openai import OpenAIEmbeddings from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.tools import tool from langchain.agents import create_tool_calling_agent, AgentExecutor from langchain.tools.retriever import create_retriever_tool # 1) 检索这半边:复用第三篇建的库 embeddings = OpenAIEmbeddings() vectorstore = Chroma(persist_directory="./db", embedding_function=embeddings) retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) retriever_tool = create_retriever_tool( retriever, "search_docs", "搜索公司内部文档,回答产品功能、退款政策等问题时使用。", ) # 2) 工具这半边:一个实时接口 @tool def get_weather(city: str) -> str: """查询城市当前天气,返回温度和状况。""" return f"{city} 22 度,多云,风力 3 级" # 3) 模型 + 提示词(含多轮占位) model = ChatOpenAI(model="gpt-4o-mini") prompt = ChatPromptTemplate.from_messages([ ("system", "你是公司客服助手,只根据工具返回的信息回答,不知道就说不知道。"), MessagesPlaceholder("chat_history"), ("human", "{input}"), MessagesPlaceholder("agent_scratchpad"), ]) agent = create_tool_calling_agent(model, [retriever_tool, get_weather], prompt) executor = AgentExecutor(agent=agent, tools=[retriever_tool, get_weather], verbose=True) ans = executor.invoke({"input": "我们的退款政策是怎样的?"}) print(ans["output"])
跑通这条,你就拥有了一个会查文档、会调接口的助手。但它现在每次都是"失忆"的,同一会话多问几句就接不上。下面补记忆。
多轮记忆怎么接
AgentExecutor 默认不存历史。要续上上下文,把历史消息在调用前塞进 chat_history,并在每轮结束后把新消息追回去。LangChain 提供了现成的包装:
from langchain_community.chat_message_histories import ChatMessageHistory from langchain_core.runnables.history import RunnableWithMessageHistory store = {} def get_history(session_id: str): if session_id not in store: store[session_id] = ChatMessageHistory() return store[session_id] chain_with_history = RunnableWithMessageHistory( executor, get_history, input_messages_key="input", history_messages_key="chat_history", ) chain_with_history.invoke( {"input": "退款政策里写明几天到账?"}, config={"configurable": {"session_id": "u_001"}}, ) chain_with_history.invoke( {"input": "那超时了怎么投诉?"}, config={"configurable": {"session_id": "u_001"}}, )
session_id 区分不同用户,get_history 按它取出对应会话的消息。第二次问"超时了怎么投诉",模型已经看得到第一轮聊过退款。生产里 store 要换成 Redis 这类外部存储,别放进程内存。
上线前还得补的几件事
- 检索质量:
k取几、块切多大,决定了答案靠不靠谱。先离线拿真实问题测一遍召回,别等用户骂了再调。 - 工具描述:docstring 写清"什么时候该用",模型才调得准。描述含糊,它要么不调、要么乱调。
- 超时与降级:工具调外部 API 会挂。
AgentExecutor配max_iterations防死循环,单个工具加自己的超时和兜底返回值。 - 成本控制:Agent 可能连调好几次模型,一次问答的 token 数不好预估。接
with_config或回调把每次用量记下来,设个上限。 - 别让它编:system 提示词钉死"不知道就说不知道",检索工具返回空时也别硬答。
这个系列到这就收了
五篇走下来,你手里有了:一套核心抽象的认知(一)、能写顺的 LCEL 链(二)、能查文档的 RAG(三)、会调工具的 Agent(四)、以及把它们拼起来的问答助手(五)。
LangChain 的接口还在变,但"模型只管生成、脏活抽象成层、用管道组合"这条主线很稳。后面你接新向量库、换模型、加工具,都是在替换某个 Runnable 节点,主干不用动。真到要管更复杂的多步状态、人机协作,再去碰 LangGraph,那是 Agent 这条线的下一站。