如果你用过某个大模型聊过天,你会发现它最大的毛病是“记性差”——聊完上句忘了下句;同时它也是“空谈家”——让它算个日期,它宁愿编一个也不愿意老老实实打开计算器;更麻烦的是,你想要的是能塞进数据库的 JSON,它却回你一段带星号的 Markdown 排版文本。这篇笔记要聊的正是 LangChain Quick Start 系列第三篇的五个核心模块:系统提示词、工具封装、上下文、结构化返回、记忆体,说白了就是解决“角色怎么设定”“模型怎么动手”“对话怎么续上”“输出怎么规整”“记忆怎么存储”这五个问题。适合已经跑通 LangChain 的最简 Demo、却不知道怎么把简单调用变成真正可用 Agent 的开发者,也算是我自己从“demo 漂亮”到“真实项目能扛”中间一堆踩坑记录的小结。
我默认你的环境满足:Python 3.10+,LangChain 版本在 0.2 以上。下面所有代码都围绕“模型只负责思考,工具负责执行,历史负责记忆”这个思路展开。
1. 为什么 Quick Start 第三篇才轮到这五个能力
1.1 前两篇跑通了什么
前两篇如果你跟过,大概率已经写出来过一个“废话机器人”:一个 PromptTemplate,往里塞变量,丢给模型,拿到输出,可能还套了一层输出解析器把文本拆成列表。那个链路长这样:
from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="你的模型名", api_key="你的API_KEY", base_url="你的兼容地址") prompt = ChatPromptTemplate.from_template("你是助理,请回答:{question}") chain = prompt | llm print(chain.invoke({"question": "今天适合穿什么?"}))这个链路本身没有错,但它暴露了三个限制:第一,模型没有“人设存储”,系统提示词如果想实现,只能靠拼接字符串,拼的时候还要小心中间注入;第二,模型没有调用工具的能力,你跟它说“帮我把这几个数加起来”,它未必真去做加法;第三,模型是无状态的,你问“刚才那句说了什么”,它真的不记得。
第三篇补的就是这些缺口,所以它放在“Quick Start”的收尾阶段非常合适:当你已经理解 chain 的管道结构,后面这些概念才不会显得生硬。
1.2 这五个名词到底在解决什么问题
很多人一上来就被术语吓住:“系统提示词”“工具封装”“上下文”“结构化返回”“记忆体”,听起来像五个独立框架。实际拆开看,它们对应的是同一个 Agent 应用里五个必然存在的环节:
| 能力 | 解决痛点 | 生活类比 |
|---|---|---|
| 系统提示词 | 模型泛泛而谈,没有任何角色和行为边界 | 给新员工发《岗位行为规范手册》 |
| 工具封装 | 模型只会说不会做,无法操作外部系统 | 给员工发电脑、计算器、电话 |
| 上下文 | 多轮对话之间没有任何关联 | 让员工能看到本次会议前面几页纪要 |
| 结构化返回 | 模型输出自然语言,程序难消费 | 要求员工必须填统一制作的 Excel 表 |
| 记忆体 | 会话结束就失忆,无法跨轮次保存状态 | 给员工一本可翻阅的客户档案 |
理解了这张表之后你会发现,LangChain 做的并不是什么玄学魔法,它只是把“大模型应用开发里一定会重复的事”做成了标准化组件。所以读完这篇之后你想要变通也很容易:不用 LangChain,用别的框架,或者干脆自己写,核心思路也是完全一样的。
1.3 一个容易被忽略的版本前提
说正事之前必须强调版本问题。LangChain 的 API 迭代速度在业内是出了名的,光“记忆体”这一个概念官方就换过三套用法。我下面写的代码,全部基于 0.2 之后推荐的方式:以langchain-core的 message 对象为基座,历史记录用RunnableWithMessageHistory管理,工具则纯粹依赖模型原生的 tool calling。如果你项目里的旧代码还在用ConversationBufferMemory(return_messages=True),请务必先升级再来对照,否则你会被DeprecationWarning刷屏。
2. 系统提示词:控制模型行为最便宜的杠杆
2.1 SystemMessage 不是开场白
新手最常见的错误,是把系统提示词当成“开场白”的一部分,拼在 user prompt 的最前面。比如:
prompt = f"你是客服,态度友善。用户说:{question}"这样做在短对话里还能凑合用,一旦对话变长、用户输入里夹带私货,问题就来了:用户那条信息很容易“覆盖”掉你开头的设定,模型会以为用户才是规则制定者。
在 LangChain 里,系统提示词的正确身份是SystemMessage,它和HumanMessage、AIMessage一样,是消息数组里的一个独立角色。底层模型对它的处理是“这是最高优先级的规则输入”,而不是“一条普通文本”。哪怕用户后续消息写得再花哨,系统提示词的优先级依然靠前,这是安全性与稳定性的第一道防线。
from langchain_core.messages import SystemMessage, HumanMessage messages = [ SystemMessage(content="你是客服助手,只能回答与订单相关的问题,其他一律礼貌拒绝。"), HumanMessage(content="帮我写一首关于秋天的诗"), ] resp = llm.invoke(messages) print(resp.content)实测里面写“拒绝其他话题”之后,模型拒答的效果非常稳定,如果你把这段话拼到普通前缀里,遇到还不错的模型有 60% 的概率它会照写不误,遇到弱一点的模型直接跑偏。
2.2 把约束写进系统提示词的常见写法
系统提示词不是越长越好。我见过有人写两千字的“角色设定”,结果模型反而把核心任务忘了。根据我的经验,一份可用的系统提示词至少应包含以下几个区块:
- 角色定义:你是什么,服务对象是谁。
- 任务边界:你负责做什么,坚决不做什么。
- 输出规则:使用什么语言、什么格式、要不要解释过程。
- 禁令列表:明确不能出现的内容,比如“不要编造订单号”。
- 少量示例:如果有条件,给一个输入输出的对齐样例。
把这个模板化在 LangChain 里很简单,直接用一个SystemMessagePromptTemplate:
from langchain_core.prompts import SystemMessagePromptTemplate system_template = """ 你是{bot_name},负责{scope}。 规则: 1. {rule_1} 2. {rule_2} 如果用户请求超出边界,请回复:{fallback_text} 请始终使用{language}回答。 """ system_prompt = SystemMessagePromptTemplate.from_template(system_template) messages = [ system_prompt.format( bot_name="订单客服小助手", scope="查询订单状态、修改收货地址、申请退换货", rule_1="不得编造订单信息,系统里查不到就明说", rule_2="不得回答与订单无关的问题", fallback_text="抱歉,这个问题超出我的服务范围。", language="简体中文", ), HumanMessage(content="我的订单到哪里了?单号是 2024001"), ]这样做的另一个好处是,你可以很轻松地在代码里维护多套“人设配置”,比如“客服版”和“销售版”共用同一套代码,只改配置不碰逻辑。
2.3 实测中我发现的两个反直觉现象
第一,模型不是完全服从系统提示词的。如果系统提示词说“不要使用工具”,但工具列表里已经绑定了函数,模型很多时候依然会去调用工具。原因在于,tool calling 层的决策发生在一个独立机制里,它只看到“当前有哪些函数可用”,系统提示词里的提醒并不总能穿透过去。所以我的经验是:想禁止某个工具,不在提示词里用嘴说,直接在绑定层面把它摘掉。
第二,temperature对系统提示词有隐蔽影响。temperature调大之后,输出会更有“创造力”,代价是系统提示词里的规则更容易被选择性忽略。我在调试一个客服机器人时,把temperature调到 1.2,系统提示词要求“必须简短回答”,结果模型每次都写长篇大论。后来把温度降到 0.3,规则遵守率明显回升。如果你发现系统提示词偶尔失效,先检查一下温度参数,别急着怀疑提示词写得不好。
3. 工具封装:把函数变成模型的手脚
3.1 用 @tool 装饰器定义一个工具
工具封装的核心价值,是把“模型的自然语言意图”和“真实代码的执行”连接起来。LangChain 0.2 之后,定义一个工具前所未有地简单:一个普通 Python 函数,加上@tool装饰器就行。
from langchain_core.tools import tool from datetime import datetime @tool def get_current_time(timezone: str = "Asia/Shanghai") -> str: """获取指定时区的当前时间。 参数 timezone 是 IANA 时区名称,例如 Asia/Shanghai。 当时区无法识别时返回错误信息。 """ try: from zoneinfo import ZoneInfo now = datetime.now(ZoneInfo(timezone)) return now.strftime("%Y-%m-%d %H:%M:%S") except Exception as e: return f"时区解析失败:{e}"这里面有几个细节决定工具好不好用。
第一,docstring 必须认真写。模型不是人,它不会读你的源码,它判断“该不该用这个工具”的唯一依据就是函数名、参数描述和 docstring。我见过很多工程团队把工具描述写得像 API 文档,比如“get_current_time(timezone: str) -> str”,模型对这种描述大概率在需要时想不起来调用。
第二,参数必须有类型标注和默认值。LangChain 会把函数签名转换成模型需要的 JSON Schema,你的类型标注越清晰,模型给出的参数就越准确。timezone: str = "Asia/Shanghai"这种写法,等于告诉模型这个参数可以省略,省略时用默认值。
第三,函数返回值尽量做成“人类可读的字符串”。你可能会想“工具返回 JSON 不是更好吗”,实际上工具返回的内容还要原样喂回给模型做后续推理,一长串 JSON 会让模型思考变慢、格式困惑。简单字符串是最佳中间格式。
3.2 工具描述怎么写,模型才不犯糊涂
工具调用的成败,一半取决于工具本身的逻辑,另一半取决于描述质量。我见过最典型的翻车案例:给模型提供了get_weather和get_news两个工具,描述都写着“获取指定城市的信息”,结果模型在用户问天气时调用了get_news。
问题就出在描述太笼统。更好的做法是把“什么时候用”和“什么时候别用”都写进去:
@tool def get_weather(city: str, date: str = "today") -> str: """查询某城市在指定日期的天气概况。 当用户询问温度、降雨、风力等天气信息时使用。 不要用这个工具查询新闻、航班或限行信息。 城市参数必须是中文城市名,例如“北京”“广州”。 """ # 模拟天气查询逻辑 return f"{city} {date}: 多云,18-26℃,东南风2级"实测下来,加上“不要用于”这半句之后,工具选择的准确率提升非常明显。我猜测原因是模型对否定边界同样敏感,给它“什么时候不该用”的约束,等于帮它划清了工具的能力边界。
3.3 不靠 AgentExecutor,自己走一遍工具调用链路
很多人第一次接触工具调用,直接上AgentExecutor,然后被里面自动执行机制的黑盒搞得一头雾水。我建议你手动拆一次工具调用链路,理解之后再看封装就通透得多。手动链路分四步:
- 调用
bind_tools把工具列表绑定到模型 - 第一次
invoke,模型返回一个带tool_calls的结果 - 你根据
tool_calls里的函数名执行对应函数,拿到执行结果 - 把结果包装成
ToolMessage喂回给模型,模型再做最终回答
from langchain_core.messages import ToolMessage, AIMessage llm_with_tools = llm.bind_tools([get_current_time, get_weather]) user_input = "现在北京时间几点?" resp = llm_with_tools.invoke([HumanMessage(content=user_input)]) # 判断模型是否想调用工具 if resp.tool_calls: tool_call = resp.tool_calls[0] print("模型想调用:", tool_call["name"], tool_call["args"]) # 根据函数名分发执行 if tool_call["name"] == "get_current_time": tool_output = get_current_time.invoke(tool_call["args"]) else: tool_output = get_weather.invoke(tool_call["args"]) # 把工具结果作为“工具消息”传回去 messages = [ HumanMessage(content=user_input), resp, ToolMessage(content=tool_output, tool_call_id=tool_call["id"]), ] final = llm.invoke(messages) print(final.content)注意一个关键点:回传ToolMessage时必须带上tool_call_id。这个 ID 是模型第一次发起工具调用时生成的,LangChain 靠它把“工具调用请求”和“工具执行结果”配对起来。很多新人会漏掉这个字段,结果模型报错“没有找到对应的 tool call”。
4. 上下文与记忆体:两层“记住”不能混为一谈
4.1 先弄清楚 messages 结构长什么样
LangChain 对话模型的所有输入输出,本质都是一串消息数组。四种最常见的角色:
- HumanMessage:用户的输入。
- AIMessage:模型的回复。如果模型调用了工具,
tool_calls也挂在它上面。 - SystemMessage:系统提示词。
- ToolMessage:工具执行结果的回传。
“上下文”这个词听起来抽象,实现起来就是“把历史消息按顺序拼进数组”。模型没有隐藏状态,它每一次看到的消息数组都是完整的对话内容。你传给它 10 轮历史,它就有 10 轮记忆;你只传最后一句,它就只有这一句记忆。
messages = [ SystemMessage(content="你是订单助手。"), HumanMessage(content="我要查询订单 2024001"), AIMessage(content="好的,请稍等,我帮你查询。"), HumanMessage(content="查到了吗?"), ]这段数组就是一次完整的“带上下文的对话”。如果你在代码里只传最后一条HumanMessage,模型第一次能把订单号记住,第二次就忘了。这个现象不是模型笨,而是你根本没把历史传给模型。
4.2 手动维护历史:最可控的笨办法
在官方各种记忆组件满天飞之前,我从一开始做多轮对话就是自己手动维护消息数组。与其说是保守,不如说这种方式最可控。核心逻辑很简单:每次用户发消息时,把历史消息从列表里取出来,拼上当前输入,一起传给模型;拿到结果后,把新的交替追加进列表。
history = [] def chat_with_history(user_text: str, max_history: int = 6) -> str: global history history.append(HumanMessage(content=user_text)) # 只保留最近 max_history 条消息,防止 token 过长 truncated_history = history[-max_history:] reply = llm.invoke(truncated_history) history.append(reply) return reply.content这个做法有一个必须注意的问题:如果把SystemMessage也塞进 history 里,截断时可能会把系统提示词切掉。所以实践上我会把系统提示词单独放,不在截断范围内。另一个问题是 token 消耗。实测中,一段 2 轮对话带历史传给模型,token 数大概是新开对话的 3 倍左右;对话时间一长,10 轮以上不带截断,账单会非常难看。所以max_history一定要设,这个参数就是记忆的“保质期”。
4.3 记忆体封装的现代姿势
LangChain 官方后来把记忆功能整合成了一个更标准的组件:RunnableWithMessageHistory。它做的事情,本质上就是“自动把消息记录到某个存储槽位,并在下一次调用时自动拼接进消息数组”。唯一的额外要求,是你需要提供一个“取历史会话”的函数,以及一个可以存放消息的历史存储对象。
from langchain_core.chat_history import InMemoryChatMessageHistory from langchain_core.runnables.history import RunnableWithMessageHistory store = {} def get_session_history(session_id: str): if session_id not in store: store[session_id] = InMemoryChatMessageHistory() return store[session_id] chain_with_history = RunnableWithMessageHistory( chain, # 你之前组好的任何 chain 或 llm get_session_history=get_session_history, input_messages_key="input", # 从输入字典里取哪个字段作为用户消息 history_messages_key="history", ) resp = chain_with_history.invoke( {"input": "我叫张三"}, config={"configurable": {"session_id": "user_001"}}, ) print(resp.content)关键在于session_id。它相当于给每个用户开了一个独立的“聊天记忆槽”。同一个 session_id 连续调用,对话会不断累积;换一个 session_id,就是新的一段对话。这套机制直接解决了“多用户记忆互相污染”的问题。
不过InMemoryChatMessageHistory是纯内存实现,进程一重启就没了,只适合开发和测试。生产环境建议把消息历史持久化到数据库或者 Redis,官方也提供了很多集成方案,原理都一样:把“消息数组读写”这个行为替换成数据库操作即可。
5. 结构化返回:把模型输出从“文章”变成“数据”
5.1 用 Pydantic 模型约束输出的形状
真实项目的下游系统很少直接消费自然语言。你可能需要模型返回一个“订单信息对象”,里面有订单号、状态、金额;或者返回一个“用户意向分类”,里面有类别和置信度。以前的做法是写一堆正则和json.loads,解析出的结果还得自己排查字段缺失。LangChain 0.2 之后更优雅的方式是with_structured_output:你定义一个数据模型,模型 API 层直接按这个结构返回。
from typing import Literal, Optional from pydantic import BaseModel, Field class OrderInfo(BaseModel): order_id: str = Field(description="订单号,必须是数字字符串") status: Literal["pending", "shipped", "completed", "cancelled"] = Field(description="订单状态") amount: Optional[float] = Field(default=None, description="订单金额,单位元,未知时为null") structured_llm = llm.with_structured_output(OrderInfo) resp = structured_llm.invoke("我的订单 2024001 现在什么状态,多少钱?") print(resp) print(type(resp))你看,输出直接就是一个OrderInfo实例。resp.order_id、resp.status、resp.amount都是可编程访问的字段,不需要一点字符串解析。Field(description=...)不是摆设,它会变成模型的输出格式说明,描述越明确,模型填对的概率越高。
5.2 结构化输出失败的三个真实原因
我从来不相信“结构化输出 100% 成功”这种话,实测中至少有三个常见坑。
第一个坑是模型本身对 JSON 模式支持不彻底。如果你的模型是通过兼容接口接入的,with_structured_output在底层走的是模型的 JSON mode 或 function calling 能力,模型不支持时就会退化成“提示词里要求输出 JSON”。这种退化通常不会报错,但返回的内容可能带着 Markdown 的 ```json 标记,解析时照样报错。解决办法:打印一次原始返回内容,确认到底是原生结构化还是降级方案。
第二个坑是字段命名歧义。比如模型要返回date和datetime两个字段,很多模型分不清差异,直接把同一个值填进去。解决办法是给字段加更精细的描述,比如Field(description="仅包含日期,格式YYYY-MM-DD,不要带时间")。
第三个坑是必填字段缺失。模型在拿不准的时候,倾向于给必填字段填一个它自认为合理的默认值,而不是老实填 null。如果你发现返回数据里混入了模型编造的值,可以在描述里强调“信息不足时返回 null,不要猜测”。
5.3 跟传统 json.loads 方式相比的取舍
过去我经常写这种代码:
raw = llm.invoke("提取订单信息并返回JSON") import json try: data = json.loads(raw.content) except Exception: # 一大堆兜底逻辑如果模型老老实实输出纯 JSON,这条路没问题;一旦输出带点解释文字,或者使用了单引号,直接json.loads就会炸掉,你不得不写各种正则去清洗。with_structured_output的价值在于它把“解析后校验”这件事从你的业务代码里剥离了,模型输出不合法时框架可以自动重试或者纠错。但它也不是万能药,如果你用的是不支持 tool calling 的模型,你会发现它实际行为还是“提示词约束 + 解析”,这种场景下老老实实自己写解析反而更可控。所以选型建议是:模型原生能力强,用with_structured_output;模型能力弱,别硬上,回归传统方式更省心。
6. 实战组装:一个能记住上下文、能调工具的客服助手
6.1 组装思路与目标效果
现在把前面所有模块拼起来,目标很具体:做一个客服助手,能记住用户之前说过的话,能通过工具拿到实时信息,并且最终给出一个结构化的订单查询结果。场景设定是用户先在对话里留下自己的城市,再问“明天天气”,最后问“我的订单状态”。
这一步的关键不是写更多代码,而是把它们按照正确的依赖关系排布:
- 系统提示词负责角色设定与任务边界。
- 工具列表让模型具备查询天气、查询订单的能力。
- 历史记录保证多轮对话上下文不丢。
- 结构化输出把最终结果规整成程序可用的对象。
- 整体流程仍然是一个 chain,用历史记录号串起来。
6.2 关键代码与解释
为了不让文章变成超长代码堆,我给出最核心的组装部分。
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.tools import tool from langchain_core.chat_history import InMemoryChatMessageHistory from langchain_core.runnables.history import RunnableWithMessageHistory # 1. 定义工具 @tool def get_weather(city: str, date: str = "today") -> str: """查询指定城市某天的天气概况。 当用户询问温度、降雨、风力等天气信息时使用。 城市必须是中文城市名。""" # 模拟数据 return f"{city} {date}: 多云转晴,22-30℃,东南风3级" @tool def get_order_status(order_id: str) -> str: """查询订单状态。 仅当订单号以 2024 开头时可查询。""" # 模拟数据 return f"订单 {order_id} 已发货,快递预计明天送达" tools = [get_weather, get_order_status] llm_with_tools = llm.bind_tools(tools) # 2. 构建一个支持历史记录的 chain prompt = ChatPromptTemplate.from_messages([ ("system", "你是客服助手,负责查询天气和订单。不要编造数据,工具没返回就明说。"), MessagesPlaceholder(variable_name="history"), ("human", "{input}"), ]) chain = prompt | llm_with_tools # 3. 用一个函数来执行工具结果并返回最终字符串 def execute_tool_calls(message: AIMessage) -> list[ToolMessage]: result_messages = [] for call in message.tool_calls: name = call["name"] args = call["args"] if name == "get_weather": output = get_weather.invoke(args) elif name == "get_order_status": output = get_order_status.invoke(args) else: output = f"未知工具 {name}" result_messages.append(ToolMessage(content=output, tool_call_id=call["id"])) return result_messages # 4. 通过 RunnableWithMessageHistory 包装,实现会话记忆 history_store = {} def get_session_history(session_id: str): if session_id not in history_store: history_store[session_id] = InMemoryChatMessageHistory() return history_store[session_id] chain_with_history = RunnableWithMessageHistory( chain, get_session_history=get_session_history, input_messages_key="input", history_messages_key="history", ) def chat_and_tool(session_id: str, user_text: str): resp = chain_with_history.invoke( {"input": user_text}, config={"configurable": {"session_id": session_id}}, ) # 如果模型产生工具调用,我们手动执行并把结果回传后再次调用 if resp.tool_calls: tool_msgs = execute_tool_calls(resp) # 将工具结果追加进当前会话历史 chat_history = get_session_history(session_id) chat_history.add_message(resp) for tm in tool_msgs: chat_history.add_message(tm) # 带着工具结果重新调用一次,得到最终回复 final_resp = llm_with_tools.invoke(chat_history.messages) # 同样把最终回复写入历史 chat_history.add_message(final_resp) return final_resp.content return resp.content这个实现有一个地方值得特意说明:我没有依赖 LangChain 高阶的 Agent 执行器,而是手动把工具结果写回会话历史,让模型基于工具结果生成最终回复。你会在很多 LangChain 例子里看到隐式的自动执行,这里手写一遍是想让你明白,所谓“智能体调用工具”的本质就是:模型发起调用请求,代码执行,结果再塞回消息流。
6.3 实测效果与仍然会翻车的地方
拿上面的函数跑一轮对话,预期的交互是:
- 第一轮,用户:“我的城市是北京”。模型此时不需要调用工具,直接回答“好的,已记住你在北京”。
- 第二轮,用户:“明天天气怎么样”。模型发现自己的消息历史里存在“北京”,于是调用
get_weather(city="北京", date="明天"),返回天气结果。 - 第三轮,用户:“查一下订单 2024001”。模型调用
get_order_status,返回订单状态。
整个链路能跑通的最大功臣是历史记录。如果没有记忆体,第二轮模型无从知道用户在哪个城市,一定会反过来问一句“你在哪个城市?”。这个例子足以说明为什么“能记住”是 Agent 应用和普通单轮问答的分水岭。
但我也要坦白几个仍然会翻车的地方。
第一,手动把resp写进历史时,如果resp是一个既有文本回复又有 tool_calls 的混合体,直接add_message(resp)会把文本也存进去,下一轮模型可能会把上一轮的中间思考当作最终答案。稳妥做法是只把最终一轮的final_resp写入历史,中间调用结果不进记忆。
第二,get_weather和get_order_status都是模拟数据,真实接入时需要在工具内部处理超时、接口异常等。工具内部一旦抛异常,异常会直接中断整个 chain。强烈建议在工具函数里写 try/except,把错误信息以字符串形式返回,这样模型至少能知道“查询失败”而不是“程序崩溃”。
第三,history_store是内存字典,单进程测试没问题,多进程部署时会各存各的。生产环境请把会话历史替换成 Redis 这类外部存储。
7. 从 Quick Start 到真实项目还要补的功课
如果你跟着上面跑通一遍,其实已经具备了一个 Agent 应用的雏形。但说实话,Quick Start 距离生产环境还很远。以我自己的经验,至少要再补三块。
第一块是 token 成本控制。带上完整历史调用一次模型的 token 消耗,大约是单轮对话的 3 到 5 倍。对话轮数一多,历史列表会被撑得很大,响应速度变慢、费用变高。比较好的做法是给历史记录设置最大条数,并配合模型的上下文窗口大小做截断;更激进的做法是用“摘要记忆”把早期对话压缩成一段摘要,而不是原封不动地全量保存。
第二块是工具鉴权与安全。给模型挂上工具之后,工具能做什么,模型就会指挥它做什么。如果你把一个“删除数据库记录”的函数挂上去,又没设置任何权限控制,那后果不堪设想。我自己的习惯是:工具函数只开放白名单操作,所有涉及写操作的工具必须带二次确认参数,或者在函数内部做权限校验。
第三块是评估。系统提示词改了、工具描述改了、模型版本换了,都会影响最终输出质量。你可以准备几十条标准测试用例,每次改动后批量跑一遍,把返回结果和期望结果做对比。没有这套评估,你永远不知道是哪次改动让回答变差了。
8. 最后分享两个我自己调试时用得最多的小技巧
第一个技巧是打开langchain.debug。在脚本最顶部设置环境变量或直接调用langchain.debug = True,LangChain 会把每次调用的详细过程打到控制台,包括消息数组、工具调用参数、返回结果。遇到“模型行为不符合预期”时,这比任何日志都好用,几秒钟就能看清是工具没被调用,还是系统提示词没生效,还是历史记录拼接错了。
第二个技巧是给每个工具和每个字段写细描述,不要偷懒。模型的表现高度依赖描述质量。我见过有人定义get_info(name: str)一个工具,想同时承担查天气和查新闻的功能,结果模型 50% 的概率会带错参数。后来拆成两个工具,一个get_weather、一个get_news,每个描述里都写清楚适用场景,准确率直接上去了。工具不是越少越好,边界清晰比数量少更重要。
我在实际项目中总结出来的体会是:LangChain 的组件越来越多,真正决定项目上限的往往不是框架本身,而是你对自己场景的理解是否足够清晰。系统提示词、工具封装、上下文、结构化返回、记忆体,这五个模块本质上是五个决策点,每个决策点都要回到你的业务里去想“这一步到底要解决什么问题”。想清楚这层,用 LangChain 还是用别的框架,反而不那么重要了。