本章涵盖:
- 使用 LangGraph checkpoint 添加短期记忆
- 在工作流的多个阶段实现 guardrails(护栏机制)
- 面向生产部署的其他注意事项
要构建能够在真实世界环境中稳定运行的 AI 智能体,绝不仅仅是把一个语言模型接上若干工具那么简单。生产系统需要在多轮交互中维持上下文、遵守应用边界、优雅地处理边界情况,并且在出现意外时仍能持续运行。没有这些保障机制,再强大的模型最终也会输出错误答案、偏题回复,或者表现出不一致的行为,从而削弱用户信任。
在本章中,我们将聚焦于让 AI 智能体具备生产可用性的两个最重要能力:记忆(memory) 和 护栏(guardrails) 。记忆使智能体能够“记住”过去的交互,从而支持自然对话、回答追问,以及在被打断后恢复上下文。护栏则把智能体约束在其预期职责范围和策略框架之内,在请求到达模型之前就过滤掉无关或不安全的输入;必要时,也可以在模型生成回复之后,拦截不恰当的输出。
我们将以已有的旅行信息助手为贯穿示例,考察这些能力在 LangGraph 中是如何工作的。你会看到如何用 checkpoint 持久化对话状态,如何在 router 层和 agent 层同时施加范围限制,以及如何把设计扩展到 post-model 检查和 human-in-the-loop(人工参与审核)流程。到本章结束时,你将掌握构建这类助手所需的工具:它们不仅聪明,而且安全、聚焦且具备韧性。
14.1 Memory
在设计基于 AI 智能体的系统时——尤其是以聊天机器人形式对外提供服务的系统——迈向生产可用的最关键一步之一,就是加入记忆。记忆让系统能够在用户交互之间保留上下文,从而支持有状态工作流和自然对话。
如果没有记忆,那么用户与大语言模型(LLM)之间的每一次交互都会重新开始,模型对之前说过什么一无所知。对于简单问答,这或许还可以接受;但只要场景涉及追问、细化,或者引用前文内容,体验就会很快变得令人沮丧。
LangGraph 提供了一种强大且灵活的机制来实现记忆:checkpoint。在本节中,我们将通过 checkpoint 来探索短期记忆,并看看如何把它集成进现有的 travel_assistant 智能体中。
14.1.1 记忆的类型
在 AI 智能体中,记忆可以存在于不同的范围层级。三种常见范围如下:
短期记忆(short-term memory) —— 在单次会话中,用户与 LLM 之间保留的上下文,通常存储在内存中,或者存放在会话级别的存储中。这非常适合支持用户在达成目标之前的持续对话。
长期用户记忆(long-term user memory) —— 跨同一用户的多次会话持久存在,使系统能够记住该用户的偏好或历史行为。
长期应用级记忆(long-term application-level memory) —— 跨所有用户与所有会话持久存在,存储对所有人都有用的通用知识,例如当前汇率。
长期用户记忆和长期应用记忆通常都高度依赖具体系统设计,因此本节我们只专注于短期对话记忆。
14.1.2 为什么需要短期记忆
在一个使用工具调用协议的对话式应用中(如前几章所述),一次典型交互通常如下:
用户发送一条消息。
LLM 可能会发起工具调用。
应用执行这些工具,并将结果返回给 LLM。
LLM 综合这些信息,生成最终答案。
如果用户接着提出一个澄清性或相关性的追问,那么一个无状态系统就会丢失之前的所有上下文,迫使整个对话重新开始。这既低效,也不自然。
解决办法是:在每次交互后保存完整的对话历史,并在后续轮次中重新喂给 LLM。这样系统就从无状态变成了有状态,模型也就能基于前文解析诸如“同一个镇”“那家酒店”这样的指代。
14.1.3 LangGraph 中的 checkpoint
虽然你完全可以通过在每次 LLM 回复后保存消息列表的方式来实现短期记忆,但 LangGraph 提供了一个更强大、更通用的机制:checkpointer。checkpoint 是图执行状态在流程某个特定时刻的快照。实际运行中,LangGraph 会在图中的每一个节点保存这种快照——从入口点(也称为 START 节点)开始,一直到退出点(END 节点)结束。
与只保存消息相比,这种方式更加灵活,因为它保留的是图状态的全部内容,而不仅仅是对话文本。这种灵活性使它除了聊天记忆之外,还能覆盖更多使用场景:
失败后的状态恢复 —— 如果执行在中途失败,你可以从最后一个成功的 checkpoint 恢复,而不必重新跑完整个流程。这在某些步骤开销昂贵或耗时较长时尤其有价值。
human-in-the-loop 工作流 —— 你可以在某个 checkpoint 暂停,收集人工输入或审批,然后基于保存的状态精确地从中断点继续执行。
多轮对话上下文 —— 对于聊天机器人而言,checkpoint 能确保对话历史在各轮之间得到保留,而无需手工重建。
在本章中,我们只关注最后一种用法——在整个用户–LLM 会话期间维护对话状态——但也要记住,同一套机制同样足以支撑前两种用例。
什么是 checkpoint?
checkpoint 表示图在某个给定 super-step 上的已保存状态,这里满足以下条件:
一个 super-step 对应一次单个图节点的执行。
如果某一步中有多个并行节点被处理,它们会被归在同一个 super-step 中。
通过在每个 super-step 上保存 checkpoint,我们可以做到:
复用当前执行状态来回答后续追问
将持久化的执行过程回放到某个特定节点——这在错误恢复或人工介入后恢复执行时很有用
LangGraph 中 checkpoint 的工作原理
LangGraph 的 checkpoint 机制围绕两个核心概念展开:
Checkpointer —— 负责捕获并存储状态快照的组件。在每一个 super-step 结束后(也就是某个节点执行完成后),checkpointer 会记录当前图状态,如图 14.1 所示。这个快照可以包含对话历史、工具输出、中间变量以及执行元数据——从而确保工作流能在任意时刻被恢复或检查。
图 14.1 带 checkpoint 的旅行助手时序图。每个节点执行后都会保存状态,并通过共享的 thread_id 在后续轮次中恢复状态。
Configuration —— 用来定义这些 checkpoint 属于哪个会话。在 LangGraph 中,一个会话被称为一个 thread:
每个 thread 都由一个 thread ID 标识(一个唯一值,通常是 UUID)。
同一个用户可以同时拥有多个活跃或历史保存下来的 thread(会话)。
configuration 会把 checkpoint 关联到某个特定 thread 上,以便系统后续知道该加载哪一个会话状态。
当你在后续调用图时传入同一个 thread ID,LangGraph 就会从该 thread 的最后一个 checkpoint 中取回状态,并从那里继续执行。或者在对话场景中,它会借此为 LLM 提供正确的历史上下文。
14.1.4 为旅行助手加入短期记忆
为了演示 LangGraph 的持久化与 checkpoint 机制是如何工作的,我们将把第 12.2 节中的基于 router 的 travel_assistant 扩展为支持短期记忆。为便于理解,我们按步骤逐步改造。首先,把现有的 main_05_01.py 复制一份,命名为 main_08_01.py。我们将在这份副本中加入持久化特性,便于你和原始版本对照。
步骤 1:原始的无状态 chat loop
先回顾一下现有的 chat loop,如代码清单 14.1 所示。在这个版本中,每次用户交互时,我们都只把新的消息作为 state 传给 travel_assistant。这里没有任何会话或连续性的概念——每一轮都被视为彼此完全独立。
代码清单 14.1 基于 router 的智能体方案中的原始 chat loop
def chat_loop(): #1
print("UK Travel Assistant (type 'exit' to quit)")
while True:
user_input = input("You: ").strip() #2
if user_input.lower() in {"exit", "quit"}: #3
break
state = {"messages":
[HumanMessage(content=user_input)]} #4
result = travel_assistant.invoke(state) #5
response_msg = result["messages"][-1] #6
print(f"Assistant: {
response_msg.content}\n") #7
#1 定义 chat loop
#2 获取用户输入
#3 检查用户是否输入 "exit" 或 "quit" 来退出循环
#4 创建初始 state,其中包含带用户输入的 HumanMessage
#5 用初始 state 调用图
#6 获取结果中的最后一条消息,也就是最终回答
#7 打印 assistant 的最终回答内容
在这种实现里,travel_assistant.invoke() 每次只收到 state,也就是一条全新的消息:
state = {"messages": [HumanMessage(content=user_input)]}
result = travel_assistant.invoke(state)
步骤 2:引入 thread ID
为了在多轮之间持久化状态,我们需要一种方式来唯一标识一段对话。LangGraph 通过 RunnableConfig 中传入的 thread ID 来完成这件事。我们可以在 chat loop 一开始就生成一个:
import uuid
thread_id = uuid.uuid1()
config = {"configurable": {"thread_id": thread_id}}
下面的代码清单展示了修订后的 chat loop:其中 thread ID 只创建一次,会被打印出来,并在每次 invoke() 调用时一起传入。
代码清单 14.2 引入 thread_id 后的修订版 chat loop
def chat_loop(): #1
thread_id=uuid.uuid1() #2
print(f'Thread ID: {thread_id}')
config={"configurable":
{"thread_id": thread_id}} #2
print("UK Travel Assistant (type 'exit' to quit)")
while True:
user_input = input("You: ").strip() #3
if user_input.lower() in {"exit", "quit"}: #4
break
state = {"messages":
[HumanMessage(content=user_input)]} #5
result = travel_assistant.invoke(state,
config=config) #6
response_msg = result["messages"][-1] #7
print(
f"Assistant: {response_msg.content}\n") #8
#1 定义 chat loop
#2 创建唯一的会话 ID
#3 获取用户输入
#4 检查用户是否输入 "exit" 或 "quit" 以退出循环
#5 用 HumanMessage 设置 state
#6 使用 state 和 config(其中包含 session ID)调用图
#7 获取结果中的最后一条消息,也就是最终回答
#8 打印 assistant 的最终回答内容
现在我们不再只是传入 state,而是传两个参数:
result = travel_assistant.invoke(state, config)
这个 config 会确保所有 checkpoint 和状态都归属于这个特定会话(thread_id)。
步骤 3:添加 checkpointer
有了 thread ID 之后,我们就可以加入一个内存型 checkpointer 来保存状态快照:
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
然后在编译图时,把这个 checkpointer 传进去,如代码清单 14.3 所示。
注意 InMemorySaver 非常适合开发、测试和快速原型验证。在生产环境中,你应当使用持久化存储后端,以确保状态在服务重启后仍然存在,并且能够在多个应用实例之间共享。LangGraph 内置提供了如 SqliteSaver(来自 langgraph-checkpoint-sqlite 包,底层为 SQLite)和 PostgresSaver(来自 langgraph-checkpoint-postgres 包,底层为 PostgreSQL)等选项;也提供对应的异步版本 SqliteSaverAsync 和 PostgresSaverAsync。在生产部署中,一般更推荐基于 PostgreSQL 的 checkpointer,因为它在可扩展性、可靠性和并发支持方面表现更好。
代码清单 14.3 加入 checkpointer 的图
graph = StateGraph(AgentState) #1
graph.add_node("router_agent", router_agent_node) #2
graph.add_node("travel_info_agent",
travel_info_agent) #3
graph.add_node("accommodation_booking_agent",
accommodation_booking_agent) #4
graph.add_edge("travel_info_agent", END) #5
graph.add_edge("accommodation_booking_agent", END) #6
graph.set_entry_point("router_agent") #7
checkpointer = InMemorySaver() #8
travel_assistant = graph.compile(
checkpointer=checkpointer) #9
#1 定义图
#2 添加 router agent 节点
#3 添加 travel info agent 节点
#4 添加 accommodation booking agent 节点
#5 添加从 travel info agent 到结束节点的边
#6 添加从 accommodation booking agent 到结束节点的边
#7 将入口点设置为 router agent
#8 实例化内存型 checkpointer
#9 编译图,并将 checkpointer 注入其中
这样一来,图就会在每次节点执行后保存快照,从而在下一轮中带着完整上下文继续运行。
步骤 4:为对话连续性配置 LLM
最后,我们还需要配置 LLM,使它在不必每次都重发整段历史的情况下引用前文。OpenAI 的 Responses API 可通过 use_previous_response_id 标志支持这一点:
llm_model = ChatOpenAI(
model="gpt-5", #1
use_responses_api=True, #2
use_previous_response_id=True) #3
#1 使用 GPT-5 模型实例化 LLM
#2 使用 Responses API
#3 使用前一个 response 的 ID 来延续对话
启用 use_previous_response_id=True 后,LangChain 的 ChatOpenAI 封装器就只会发送前一个响应的 ID,由 OpenAI 在内部恢复完整历史。
注意 如果你启用了 use_responses_api=True,但没有启用 use_previous_response_id=True,LangChain 会尝试在每一轮都重新发送完整历史,而不是只发送其 ID。Responses API 会把这视为重复提交并返回错误。在将 LangGraph memory 与 Responses API 一起使用时,启用 use_previous_response_id 是强制要求。
14.1.5 运行带 checkpointer 的助手
现在,我们已经把 checkpointer 集成进 travel_assistant,接下来可以真正看到短期对话记忆是如何工作的。运行更新后的基于 router 的助手,并输入一个普通问题:
Thread ID: e683b337-752b-11f0-84a9-34f39a8d3195
You: What's the weather like in Penzance?
Assistant: [{'type': 'text', 'text': 'The weather in Penzance is currently sunny with a temperature of 19°C.', 'annotations': []}]
接着输入一个会引用前文的追问(注意,这个脚本是基于 main_05_01.py 的副本,而那个版本使用的是会随机返回天气状况的 mock 天气服务):
You: What's the weather in the same town now?
Assistant: [{'type': 'text', 'text': 'Current weather in Penzance: foggy, around 28°C.', 'annotations': []}]
注意,assistant 正确地把 “same town” 解析成了 Penzance。它之所以能做到这一点,是因为 LangGraph 的 checkpointer 向 LLM 提供的是完整的对话历史,而不仅仅是最新一轮用户输入。(如前所述,由于这里使用的是 mock 天气服务,第二次调用会返回不同的天气情况。)
恭喜——你刚刚实现了一个有状态的对话式聊天机器人!虽然这已经很令人满意,但我们仍值得进一步深入看看底层发生了什么,以便真正理解 checkpointer 是如何维持短期记忆的。
14.1.6 将状态回退到过去的 checkpoint
为了更好地理解 LangGraph 是如何管理对话记忆的,我们将手动模拟它在从 checkpoint 恢复状态时内部发生的事情,步骤如下:
向 chatbot 提一个问题。
从 checkpointer 中取回最近一次 checkpoint。
将图状态恢复到该 checkpoint。
再提一个依赖该恢复上下文的追问。
这实际上就是 LangGraph 在你后续轮次中传入相同 thread_id 时自动执行的事情。
步骤 1:更新 chat loop 以便检查 state
把 main_08_01.py 再复制一份,命名为 main_08_02.py。将 chat_loop() 替换为代码清单 14.4 中的实现。
代码清单 14.4 逐步检查 state
def chat_loop():
thread_id=uuid.uuid1() #1
print(f'Thread ID: {thread_id}')
config={"configurable":
{"thread_id": thread_id}} #2
user_input = input("You: ").strip() #3
question = {"messages":
[HumanMessage(content=user_input)]} #4
result = travel_assistant.invoke(
question, config=config) #5
response_msg = result["messages"][-1] #6
print(
f"Assistant: {response_msg.content}\n") #7
state_history = travel_assistant.get_state_history(
config) #8
state_history_list = list(state_history) #9
print(f'State history: {state_history_list}') #9
last_snapshot = list(state_history_list)[0] #10
print(f'Last snapshot: {last_snapshot.config}')
thread_id = last_snapshot.config[
"configurable"]["thread_id"] #11
last_checkpoint_id = last_snapshot.config[
"configurable"]["checkpoint_id"] #12
new_config = {"configurable": #13
{"thread_id": thread_id,
"checkpoint_id": last_checkpoint_id}}
retrieved_snapshot = travel_assistant.get_state(
new_config) #14
print(
f'Retrieved snapshot: {retrieved_snapshot}') #15
travel_assistant.invoke(None,
config=new_config) #16
new_question = {"messages": [HumanMessage(
content="What is the weather in the same town?")]}
result = travel_assistant.invoke(new_question,
config=new_config) #17
response_msg = result["messages"][-1] #18
print(
f"Assistant: {response_msg.content}\n") #19
#1 创建唯一的 thread ID
#2 用 thread ID 创建 config
#3 获取用户输入
#4 创建初始 state,其中包含带用户输入的 HumanMessage
#5 使用 state 和 config 调用图
#6 获取结果中的最后一条消息,也就是最终回答
#7 打印 assistant 的最终回答内容
#8 从图中获取 state history
#9 打印 state history
#10 从 state history 中获取最新快照
#11 从最新快照中获取 thread ID
#12 从最新快照中获取 checkpoint ID
#13 用 thread ID 和 checkpoint ID 创建新的 config
#14 使用新的 config 从图中获取对应 checkpoint 的状态
#15 打印取回的快照
#16 将图回退到该 checkpoint
#17 在该 checkpoint 的上下文基础上提一个新的追问
#18 获取结果中的最后一条消息,也就是最终回答
#19 打印 assistant 的最终回答内容
步骤 2:运行示例并查看 state history
以调试模式运行 main_08_02.py,并输入常见问题(记住,这里的天气仍然是随机的):
You: What's the weather like in Penzance?
Assistant: [{'type': 'text', 'text': 'It’s currently foggy in Penzance with a temperature around 27°C.', 'annotations': []}]
随后,代码清单 14.4 中的脚本会继续执行,并取回 state history:
state_history = travel_assistant.get_state_history(config)
state_history_list = list(state_history)
print(f'State history: {state_history_list}')
你会看到多个 StateSnapshot 条目,它们每一个都代表一个 checkpoint,顺序从最近的开始,一直回溯到最初的 START 节点。每个 snapshot 都包含:
到目前为止交换过的消息
工具调用信息(如果有)
图执行步骤的元数据
步骤 3:从某个特定 checkpoint 恢复状态
接着,来看剩余脚本在做什么。首先取最近的那个 snapshot:
last_snapshot = list(state_history_list)[0]
print(f'Last snapshot: {last_snapshot.config}')
提取其中的 thread_id 和 checkpoint_id:
thread_id = last_snapshot.config["configurable"]["thread_id"]
last_checkpoint_id = last_snapshot.config["configurable"]["checkpoint_id"]
构造一个指向该 checkpoint 的新 config:
new_config = {"configurable":
{"thread_id": thread_id,
"checkpoint_id": last_checkpoint_id}}
再从图中取回这个 checkpoint 对应的状态,以确认它符合预期:
retrieved_snapshot = travel_assistant.get_state(new_config)
print(f'Retrieved snapshot: {retrieved_snapshot}')
你应该能看到直到该 checkpoint 为止的完整对话历史,包括用户消息、工具输出和 assistant 回复。
步骤 4:从恢复的状态继续执行
要把图回退到那个时间点,调用如下:
travel_assistant.invoke(None, config=new_config)
然后再问一个依赖该上下文的追问:
new_question = {"messages": [HumanMessage(
content="What is the weather in the same town?")]}
result = travel_assistant.invoke(new_question, config=new_config)
response_msg = result["messages"][-1]
你可能会看到类似这样的输出:
Assistant: [{'type': 'text', 'text': 'In Penzance it’s currently windy with a temperature around 20°C.', 'annotations': []}]
assistant 正确推断出 “same town” 指的是 Penzance,这证明恢复出来的 state 的确被重新传回给了 LLM。
通过这样一次手动回退与恢复,你现在就能从底层理解 LangGraph 的 checkpointer 是如何支撑短期对话记忆的:它会在每个节点上保存完整的执行上下文,并在之后恢复它,从而精确地从上次对话中断的位置继续。
14.2 Guardrails
Guardrails(护栏)是应用层机制,用来确保 AI 智能体始终运行在既定范围、策略框架和预期目标之内。它们相当于智能体行为的“交通规则”,会在关键节点检查和验证输入与输出,从而确保系统保持安全、相关、合规且高效。没有 guardrails,智能体可能会漂移到无关主题、生成不安全或不合规的回复,或者在不必要的动作上浪费大量处理资源。
一个设计良好的 guardrail 体系,可以阻止旅行助手去提供股市建议,防止客服机器人泄露机密信息,或者在请求到达昂贵的语言模型之前就拦截格式糟糕的输入。实践中,guardrails 通常可以分成三大类:
基于规则(rule-based) —— 通过显式条件或正则表达式来捕捉被禁止的模式或主题。
基于检索(retrieval-based) —— 通过核对被批准的数据源,确认请求是否相关、是否在范围内。
基于模型(model-based) —— 使用轻量分类模型或 moderation 模型,判断意图、安全性或是否符合策略。
这些控制可以被放在 agent 工作流的多个位置:
Pre-model checks —— 在 LLM 运行之前,拒绝无效或无关的查询。
Post-model checks —— 在输出交付之前,核验生成结果是否符合策略或安全要求。
Routing-stage checks —— 决定某个查询是否应该触发特定工具或分支。
Tool-level checks —— 阻止不安全或未授权的工具动作。
从功能上看,guardrails 就像一层验证层——如果某个检查失败,agent 的正常流程就会被调整:例如拒绝请求、要求澄清,或者改走一条更安全的响应路径。
在本节中,我们会把自定义 guardrails 集成到前面构建的旅行信息助手的 router 节点中(此时它已经增强了 memory),从而能够立即拒绝无关或超出范围的请求——例如非旅行类问题。我们还会探索 LangGraph 的 pre-model hook,它允许我们在任何 LLM 调用之前施加 guardrails,从而确保无论是 travel agent 还是 weather agent,都始终处于覆盖范围之内——例如只处理 Cornwall 的目的地。
14.2.1 实现用于拒绝非旅行类问题的 guardrails
对于我们的英国旅行信息助手来说,第一道也是最显然的一道 guardrail,就是领域相关性检查——也就是在任何 agent 推理发生之前,先对用户问题做预筛选。如果问题不在助手的职责范围内,我们就提前拦截,并礼貌地拒绝回答。这样做可以避免系统去尝试处理诸如体育赛果、金融市场或娱乐八卦之类的无关主题。引入这层 guardrail 有两个关键好处:
提升准确性 —— 我们的 agent 仅针对旅行和天气信息进行了训练和配置。如果去回答无关问题,结果几乎必然会出现不准确甚至 hallucination。通过直接拒绝无关请求,我们可以让对话始终保持在 agent 真正擅长的能力边界内。
控制成本 —— 如果没有这层过滤,一些用户可能会故意把我们的助手当作免费入口,绕过订阅成本,把它当成昂贵 LLM 的通用问答接口来使用。通过尽早拦截非旅行问题,我们能防止这种资源滥用,并避免不必要的处理成本。
定义 guardrail 策略
第一步,是明确界定什么样的问题才算在这个助手的职责范围内。这样 guardrail 才能拥有清晰、无歧义的判定标准。
首先,把上一节中的 main_08_01.py 复制一份,保存为 main_09_01.py。然后实现代码清单 14.5 中展示的 guardrail policy。
代码清单 14.5 将问题限制为旅行相关主题
class GuardrailDecision(BaseModel): #1
is_travel: bool = Field(
...,
description=(
"""True if the user question is about travel information:
destinations, attractions,
lodging (hotels/BnBs), prices, availability,
or weather in Cornwall/England."""
),
)
reason: str = Field(...,
description="Brief justification for the decision.")
GUARDRAIL_SYSTEM_PROMPT = ( #2
"""You are a strict classifier. Given the user's
last message, respond with whether it is
travel-related. Travel-related queries
include destinations, attractions, lodging (hotels/BnBs),
room availability, prices, or weather in Cornwall/England."""
)
REFUSAL_INSTRUCTION = ( #3
"""You can only help with travel-related questions
(destinations, attractions, lodging, prices,
availability, or weather in Cornwall/England).
The user's request is not travel-related.
Politely refuse and briefly explain what
topics you can help with."""
)
llm_guardrail = llm_model.with_structured_output(
GuardrailDecision) #4
#1 定义 GuardrailDecision 模型
#2 定义 GUARDRAIL_SYSTEM_PROMPT,将模型约束为只判断旅行相关问题
#3 定义 REFUSAL_INSTRUCTION,用于礼貌拒绝非旅行类问题
#4 使用同一个基础模型,通过结构化输出做快速、轻量级分类
这段代码定义了一套 guardrail policy,它使用一个轻量 LLM 分类器来判断用户问题是否与旅行相关。整个 guardrail 由几个关键部分组成,每部分都在施加领域约束和引导模型行为方面扮演不同角色:
GuardrailDecision 是一个 Pydantic 模型,用于结构化分类结果。is_travel 用来表示请求是否处于旅行领域之内,而 reason 则给出简要判断依据。
GUARDRAIL_SYSTEM_PROMPT 指示模型进行严格分类,并为我们这里的“旅行相关”给出精确定义。
REFUSAL_INSTRUCTION 包含一条固定的、礼貌的说明,用于告诉用户为什么这个问题不能回答。
llm_guardrail 则把基础 LLM 封装成结构化输出模式,从而在主路由逻辑运行之前,实现快速、稳定的判定。
更新 router graph
从高层看,我们希望所有无关请求都能立刻退出工作流——完全不触达任何下游 agent。因此,我们需要在 LangGraph 的路由结构里引入一个专门的 guardrail_refusal 节点。这个节点本身不做任何实际工作,只是直接跳转到图的 END。更新后的工作流如图 14.2 所示。
现在,router agent 会先检查一个问题是否在范围内。如果在范围内,就像之前一样,把查询路由到旅行信息 agent 或天气 agent;如果不在范围内,router 就会把它送到新的 guardrail_refusal 节点,而这个节点直接连接到 END。对应的图定义见代码清单 14.6。
图 14.2 更新后的旅行助手工作流。guardrail 引入了一条提前退出路径,使超出范围的查询在到达下游 agent 之前就终止。
代码清单 14.6 在 router graph 中添加 guardrail_refusal 节点
def guardrail_refusal_node(state: AgentState): #1
return {}
graph = StateGraph(AgentState)
graph.add_node("router_agent", router_agent_node)
graph.add_node("travel_info_agent", travel_info_agent)
graph.add_node("accommodation_booking_agent", accommodation_booking_agent)
graph.add_node("guardrail_refusal",
guardrail_refusal_node) #2
graph.add_edge("travel_info_agent", END)
graph.add_edge("accommodation_booking_agent", END)
graph.add_edge("guardrail_refusal", END) #3
graph.set_entry_point("router_agent")
checkpointer = InMemorySaver()
travel_assistant = graph.compile(checkpointer=checkpointer)
#1 定义 guardrail refusal 节点,这是一个 no-op 节点,用于直接跳到 END
#2 添加 guardrail refusal 节点
#3 添加从 guardrail refusal 节点到结束节点的边
正如你看到的,guardrail_refusal 节点本身刻意设计成 no-op——它唯一的职责,就是提供一条干净的捷径通往 END。
更新 router agent
现在,guardrail policy 已经定义好了,图也已经更新,最后一步就是实现真正执行这套策略的逻辑。之前,我们的 router agent 只负责决定把查询送给 travel agent 还是 weather agent;现在,它必须先运行一层 guardrail 检查,如图 14.3 所示。
图 14.3 带有 guardrail 检查的更新版 router 逻辑流程图:查询会先经过相关性过滤,不相关的请求会带着拒绝消息直接发送到 guardrail_refusal 节点。
如果 guardrail 检查失败——也就是说,这个问题被判定为与旅行助手无关——那么 router 就会生成一条拒绝消息,并把执行直接路由到 guardrail_refusal 节点,如图 14.3 所示。对应逻辑实现见代码清单 14.7。
代码清单 14.7 带 guardrail 执行逻辑的 router agent
def router_agent_node(state: AgentState) -> Command[AgentType]:
"""Router node: decides which agent should handle the user query."""
messages = state["messages"]
last_msg = messages[-1] if messages else None
if isinstance(last_msg, HumanMessage):
user_input = last_msg.content
# Guardrail classification at routing time
classifier_messages = [
SystemMessage(content=GUARDRAIL_SYSTEM_PROMPT),
HumanMessage(content=user_input),
]
decision = llm_guardrail.invoke(
classifier_messages) #1
if not decision.is_travel: #2
refusal_text = ( #3
"""Sorry, I can only help with travel-related
questions (destinations, attractions, lodging,
prices, availability, or weather in Cornwall/England).
Please rephrase your request to be travel-related."""
)
return Command( #4
update={"messages": [AIMessage(content=refusal_text)]},
goto="guardrail_refusal",
)
router_messages = [
SystemMessage(content=ROUTER_SYSTEM_PROMPT),
HumanMessage(content=user_input)
]
router_response = llm_router.invoke(router_messages)
agent_name = router_response.agent.value
return Command(update=state, goto=agent_name) #5
return Command(update=state, goto=AgentType.travel_info_agent)
#1 定义 guardrail 判定提示
#2 调用 guardrail 模型,返回一个 GuardrailDecision 对象
#3 检查判定结果是否不是旅行相关
#4 定义拒绝文本
#5 返回一个命令:将拒绝消息写入 state,并跳转到 guardrail refusal 节点
在这个更新后的逻辑中:
router 会先用最新的用户查询调用 llm_guardrail。
如果分类结果表明该问题不是旅行相关,router 就会构造一条固定拒绝消息,把它写入 state,并将执行送往 guardrail_refusal 节点——从而绕过所有正常路由。
如果检查通过,router 就继续执行原先的流程,选择最合适的 agent。
测试 guardrail
想看它实际运行的效果,请以调试模式运行 main_09_01.py,并在 llm_guardrail 被调用的那一行打一个断点。然后输入下面这个查询:
UK Travel Assistant (type 'exit' to quit)
You: Can you give me the latest results of Inter Milan?
当执行停在断点处时,检查 decision.is_travel。你应该会看到它是 False,因为足球比分不属于允许的旅行领域。继续执行(按 F5)后,你会得到如下输出:
Assistant: Sorry, I can only help with travel-related questions (destinations, attractions, lodging, prices, availability, or weather in Cornwall/England). Please rephrase your request to be travel-related.
恭喜——你已经成功实现了第一道 guardrail!不过,我们的工作还没结束。别忘了,我们的某个 agent 其实只覆盖 Cornwall,而不是整个英国。这意味着我们还需要在 agent 层实现更细的范围限制,这也是下一节要处理的内容。
14.2.2 在 agent 层实现更严格的 guardrails
在传统软件开发中,一个普遍的最佳实践是:每个类或组件都应自行验证输入数据,而不是完全依赖 UI 等上层来做校验。相同原则同样适用于 agent 系统:即使在 chatbot 入口处已经有更宽泛的检查,每个 agent 仍然应该在自己的输入上执行 guardrails。
这些 agent-level guardrails 往往比系统级 guardrails 更严格,因为它们可以针对某个特定 agent 的能力和数据覆盖范围来做约束。对于我们的系统来说:
旅行信息 agent 只能处理 Cornwall 相关查询,因为它的向量库只包含该地区的数据。
住宿预订 agent 目前也同样只限制在 Cornwall,以保持整个助手的能力范围一致。
因此我们有两层 guardrails:
Router-level —— 在任何 agent 逻辑或工具调用发生之前,作为 fail-fast 过滤器尽早拦截。
Agent-level —— 作为一种“belt-and-suspenders”(双保险)机制:即便某个 agent 被直接调用,或者在别的上下文中被复用,它也能拦截超出范围的请求。
定义仅限 Cornwall 的 guardrail policy
首先,我们要明确这层策略:只允许处理与 Cornwall 相关的旅行问题。这样一来,无论是旅行信息 agent 还是住宿预订 agent,对于其他地区或国家的查询都会直接拒绝。
把 main_09_01.py 再复制一份,改名为 main_09_02.py。然后,加入下面这些 system prompt,用来定义分类和拒绝行为。
代码清单 14.8 用于分类和拒绝行为的 system prompts
AGENT_GUARDRAIL_SYSTEM_PROMPT = (
"""You are a strict classifier. Given the user's last message,
respond with whether it is travel-related. Travel-related
queries include destinations, attractions, lodging
(hotels/BnBs), room availability, prices, or weather in
Cornwall/England. Only accept travel-related questions covering
Cornwall (England) and reject any questions from other areas in
England and from other countries"""
)
AGENT_REFUSAL_INSTRUCTION = (
"""You can only help with travel-related questions
(destinations, attractions, lodging, prices,
availability, or weather in Cornwall/England). The user's
request is not travel-related. Or it might be a travel
related question but not focusing on Cornwall (England).
Politely refuse and briefly explain what
topics you can help with."""
)
创建 agent-level guardrail 函数
agent guardrail 被实现为一个 Python 函数:它接收当前图状态,如果输入合法,就返回一个未修改的 state;如果输入不合法,就返回一个经过修改的 state,以指示 LLM 发出拒绝回复。代码如代码清单 14.9 所示。
代码清单 14.9 agent-level guardrail 函数
def pre_model_guardrail(state: dict):
messages = state.get("messages", [])
last_msg = messages[-1] if messages else None
if not isinstance(last_msg, HumanMessage): #1
return {}
user_input = last_msg.content
classifier_messages = [ #2
SystemMessage(content=AGENT_GUARDRAIL_SYSTEM_PROMPT),
HumanMessage(content=user_input),
]
decision = llm_guardrail.invoke(classifier_messages)
if decision.is_travel: #3
# Allow normal flow; do not modify inputs
return {}
return {"llm_input_messages":
[SystemMessage(content=AGENT_REFUSAL_INSTRUCTION),
*messages]} #4
#1 确认最后一条消息确实来自用户
#2 构造分类提示
#3 如果输入合法,则不做修改,正常继续
#4 如果输入不合法,则在 LLM 输入前面加上拒绝指令
pre_model_guardrail() 函数会在 LLM 真正看到用户查询之前,作为一个预处理过滤器执行,其逻辑如下:
先确认最新消息确实来自用户。
把用户查询连同严格的分类 system prompt 一起发给 guardrail LLM。
如果查询在范围内(既是旅行相关,又限定在 Cornwall),就原样通过。否则,就在消息前追加一条拒绝指令,让 agent 礼貌拒绝该请求。
将 guardrail 注入 agents
LangGraph 的 ReAct agents 支持 pre_model_hook 和 post_model_hook,允许你在输入或输出阶段拦截并改写内容。虽然这些 hook 也可用于诸如长输入总结、输出净化等任务,但这里我们只聚焦于输入侧的 guardrails。为了启用 Cornwall 限制,我们只需要把 pre_model_guardrail 传给 travel information agent 和 accommodation booking agent,如下所示。
代码清单 14.10 带 Cornwall guardrail 的旅行信息 agent
travel_info_agent = create_react_agent(
model=llm_model,
tools=TOOLS,
state_schema=AgentState,
prompt="""You are a helpful assistant that can search travel
information and get the weather forecast. Only use the tools
to find the information you need (including town names).""",
pre_model_hook=pre_model_guardrail, #1
)
#1 Guardrail:检查用户输入是否既与旅行相关,又聚焦于 Cornwall(England)
代码清单 14.11 带 Cornwall guardrail 的住宿预订 agent
accommodation_booking_agent = create_react_agent(
model=llm_model,
tools=BOOKING_TOOLS,
state_schema=AgentState,
prompt="""You are a helpful assistant that can check hotel
and BnB room availability and price for a destination in
Cornwall. You can use the tools to get the information you
need. If the users does not specify the accommodation type,
you should check both hotels and BnBs.""",
pre_model_hook=pre_model_guardrail, #1
)
#1 Guardrail:检查用户输入是否既与旅行相关,又聚焦于 Cornwall(England)
测试 Cornwall guardrail
以调试模式运行 main_09_02.py,并在 pre_model_guardrail() 中调用 llm_guardrail 的那一行打断点。然后尝试如下输入:
UK Travel Assistant (type 'exit' to quit)
You: Can you give me some travel tips for Liverpool (UK)?
当执行停在断点处时,检查 decision.is_travel —— 它应该是 False,因为这个查询虽然和旅行相关,但并不聚焦于 Cornwall。于是系统会在输入前加上拒绝指令,最终得到类似如下输出:
Assistant: [{'type': 'text', 'text': 'Sorry—I can only help with travel questions focused on Cornwall (England), such as destinations, attractions, lodging, prices/availability, and local weather. If you’d like tips for places like St Ives, Newquay, Falmouth, Penzance, Padstow, or Truro, tell me your interests and dates/budget and I’ll tailor suggestions.', 'annotations': []}]
至此,我们就有了两层防御:
一个 router-level guardrail,用来快速拒绝任何非旅行类问题
一组 agent-level guardrails,用来对旅行和住宿请求施加 Cornwall 范围限制
现在,这个 agentic workflow 已经同时防住了无关问题和超出覆盖范围的问题,使系统更安全、更可靠,也更可能节省成本,因为它避免了用户用 chatbot 去处理本来就不该处理的问题。
14.3 Beyond this chapter
到这里,你已经看到如何为 AI agents 配置 memory 和 guardrails——这是让它们具备生产可用性的两块最关键基石。但根据你的应用领域、规模以及合规要求,在真正面向用户部署之前,你可能还需要处理其他方面的事项。
本书中有些主题此前已经被零散提到过,但没有展开深入讲解,要么是因为它们高度依赖具体领域,要么是因为它们本身值得单独成章。下面几个方向,都是非常值得继续深入探索的。
14.3.1 Long-term user and application memory
在本章中,我们关注的是短期到中期记忆——即在单次对话中,或有限会话历史中,追踪相关状态。然而,生产环境中的 agent 往往还会从持久化的长期记忆中受益:它可以跨越数周、数月,甚至数年,保存用户偏好、过往交互和上下文信息。这可能涉及以下内容:
为每个用户维护专属向量库
定期进行总结与裁剪,以保持记忆规模可控
针对个人可识别信息(PII)实施隐私与合规控制
长期记忆可以极大增强个性化体验,但同时也会带来工程、扩展性和监管方面的挑战,你需要提前规划。
表 14.1 总结了 AI agent 系统中可能需要支持的不同记忆类型,以满足用户需求。
表 14.1 AI agents 中的记忆类型
| Memory type | Scope | Persistence | Example (travel assistant) | Challenges |
|---|---|---|---|---|
| Short-term | 单个用户会话 | 持续到会话结束 | 在同一段对话里,天气追问时记住 “same town” 指的是什么 | 连续性有限;会话关闭后即丢失 |
| Long-term (user) | 单个用户的多次会话之间 | 数周、数月或数年 | 记住某个用户偏好的 Cornwall 目的地或住宿类型 | 隐私合规,以及对持久化数据的管理 |
| Long-term (application) | 所有用户与所有会话之间 | 持续存在 | 存储通用旅行更新信息(例如 Cornwall 活动日历、季节性景点开放安排) | 保持数据新鲜;避免过时或错误信息 |
14.3.2 Human-in-the-loop
即便是一个设计良好的旅行信息助手,也一定会遇到单靠自动化无法妥善处理的情况——例如查询语义模糊、可用数据不完整,或者某个决策会对现实世界产生显著影响。在这种时候,human-in-the-loop 机制就可以把请求升级给人工旅行专家审核,再决定是否发送回复。对于我们这个聚焦 Cornwall 的旅行助手来说,典型的人审场景包括:
用户请求高度个性化的行程规划,而其中包含一些非常规活动组合,导致安全性、可行性或时间安排存在不确定性
关于实时中断事件的查询,例如恶劣天气、交通罢工或活动取消,此时需要依赖最新的人类判断
特殊住宿需求或无障碍需求,此时需要向本地服务提供方核实具体细节
在生产初期,human-in-the-loop 尤其有价值,因为它能帮助确保准确性、避免声誉受损,并从真实使用中收集反馈,以持续改进自动化策略。随着时间推移,人工审核中得到的洞察又可以被反向吸收进更好的 guardrails、更优的 prompts,或者更丰富的知识库之中,从而减少未来需要升级人工的案例数量。
14.3.3 Post-model guardrails
在本章中,我们关注的主要是在调用 LLM 之前运行的 guardrails——它们负责筛查问题的相关性,并把超出范围的请求重定向出去。而在一个生产级旅行信息助手中,你往往还会希望加入 post-model guardrails:也就是在模型输出展示给用户、或者发送给下游服务(例如预订 API)之前,对它进行额外检查。对于我们聚焦 Cornwall 的助手,这些 post-model guardrails 可能包括:
过滤过时或错误的细节,例如删除对已经永久关闭景点或已经结束活动的引用
屏蔽敏感信息,例如某些小型 B&B 的私人联系电话,这类信息应只在确认预订后共享
强制统一品牌语气与风格,确保所有旅行建议都以温暖、友好、简洁的方式表达,并与助手的人设一致
校验结构化输出,确保所有预订建议、价格报价或行程安排都符合其他系统所期望的格式
post-model guardrails 相当于最后一道安全网,用来捕捉那些“看起来合理,但其实含有事实错误、语气不符,或者不适合立即交付给用户”的输出。
14.3.4 AI agents 与应用的评估
在让一个 agent 进入生产环境之前,最容易被忽视、但又绝对不可或缺的一步,就是系统化评估。这是一个广泛而持续演化的领域,本身复杂到足以写成一本完整的书;但它对保证你的旅行助手在上线后依旧准确、安全且高效至关重要。对于我们这个聚焦 Cornwall 的旅行信息助手,评估可能包括:
功能测试(Functional testing) —— 验证助手在一大批测试查询下,是否能给出正确、相关且完整的回答,例如 “Best family-friendly beaches in Cornwall” 或 “Current weather forecast for St Ives.” 这能确保它始终在职责范围内运行,并检索到准确、最新的信息。
行为测试(Behavioral testing) —— 确认助手遵循策略和安全规则,不提供无关或不安全的旅行建议,并维持适合旅游与客服场景的一致、友好的语气。
性能测试(Performance testing) —— 在逼真的用户负载下测量延迟和 API 成本,例如旅游旺季时,查询量可能会激增。
回归测试(Regression testing) —— 当 prompt 被调整、工具被更新,或底层 LLM 被替换时,验证助手的可靠性是否仍然得到保持。通常做法是持续用一套预定义 ground truth 数据去测试系统。
虽然本书不会详细展开评估框架和方法论,但你应该把评估视为上线前 checklist 的核心组成部分,以及上线后持续进行的工作。持续评估能帮助你在用户发现问题之前先发现问题,也能帮助你适应本地事件、服务变化,并长期维持旅行助手的可信度。
为 agents 增加 memory 和 guardrails,是迈向生产化的重要里程碑,但这还只是整个旅程的一部分。真正的生产可用,需要一种更整体的视角:同时考虑安全、合规、可靠性和持续评估。好消息是,本章打下的基础,会让你在应用规模和复杂度增长时,更容易把这些额外能力一层层叠加进去。
14.3.5 在 LangGraph Platform 和 Open Agent Platform 上部署
让 agents 进入生产的最后一步,就是部署。你的部署方式会深受组织基础设施策略的影响——应用是运行在本地机房(on-premises)还是云端,以及 IT 在隐私、安全与合规方面的政策要求。同时,这也体现了组织层面的选择:有些团队偏好本地 DevOps / SRE 驱动的部署方式,而另一些团队则更倾向于使用 SaaS 托管,以换取简化运维和更好的可扩展性。
一旦你开发好了基于 LangGraph 的多智能体系统,一个自然的路径就是把它部署到 LangGraph Platform 上——这是 LangChain 为 agentic 应用提供的全托管托管方案。它内建了诸如水平扩展、持久化状态管理,以及通过熟悉的 LangSmith dashboard 提供端到端监控等能力。这个平台把大量运维负担抽象掉,让你能以极小摩擦从原型走向生产,同时仍然保有可观测性和细粒度调试能力。
另一个强有力的选择,是把你的 agents 集成进 Open Agent Platform(OAP) 。OAP 是一个灵活的 AI agent 运行时与编排层,内置了一组预构建的 agent 模式,包括 multi-tool agent 和 supervisor agent。这些模式都可以根据企业场景进行定制和扩展——无论是接入 MCP 服务器、本地向量库,还是其他企业数据源。OAP 被设计成连接“自定义 LangGraph agents”与“更广泛可组合、可互操作 agent 生态”的桥梁,因此对于那些计划让多个 agents 协同运行的组织尤其有价值。
LangGraph Platform 和 OAP 都既可以作为全托管 SaaS 提供,也可以部署到客户自己的云环境中,满足那些对数据驻留和合规控制要求更高的团队。这样的双重部署模式意味着,你可以一开始用托管方案快速启动,而在监管或运维需要时,再迁移到私有化环境。综合来看,这两条部署路径共同构成了一条平滑的连续体——从你本地电脑上的开发,到可扩展的生产托管,再到企业级的 agent 编排——让你可以在控制力、便利性和运维复杂度之间选择最适合自己的平衡点。
Summary
-
对话记忆会在多轮之间保存消息历史。比如,用户先问 “What’s the capital?”,再问 “What about population?”,系统就需要记住第一轮里说的是 “capital of France”。
-
LangGraph checkpoints 会在每个节点执行后保存完整的 agent 状态,从而支持恢复执行和分支执行。checkpoint 存储的内容包括对话历史、工具输出以及中间推理状态。
-
你可以暂停一个长时间运行的工作流,在出错后从断点恢复,或者从任意 checkpoint 分叉出新的对话线程。这支持类似 “Show me what would happen if I chose option B instead.” 这样的场景。
-
输入 guardrails 会拦截或重定向那些超出 agent 职责范围的查询。比如,一个客服 agent 会拒绝帮助用户写创意小说,或提供医疗建议。实现方式通常包括分类模型或关键词过滤器,在处理前就识别出离题请求。这样可以避免浪费 LLM 调用,也能让 agent 始终保持聚焦。
-
分层 guardrails 会在多个阶段施加检查。路由层负责拦截明显违规内容(如脏话、恶意提示注入),检索层负责过滤敏感文档,输出层则会在交付前验证回复。
-
输出 guardrails 会在回复交付给用户之前验证 agent 输出。它们可以检查是否存在 hallucinated citation、带偏见的语言、泄露的敏感数据,或者格式错误。
-
human-in-the-loop 工作流会把特定案例转交给人工操作员审批,然后才执行动作。高价值交易、敏感数据访问,或 agent 本身不确定的决策,都应触发 human-in-the-loop 审核。
-
被批准和被拒绝的案例,都会反过来成为改进自动决策阈值的训练数据。应记录所有 human-in-the-loop 决策,以便优化分类模型,并减少未来的升级数量。
-
agent 评估覆盖多个维度。功能测试验证答案是否正确,行为测试检查是否存在偏见或不安全输出,性能测试则测量每次查询的延迟和成本。
-
带有正确/错误标签的评估数据集,可以支持你对新模型版本进行自动打分。应在多轮评估中持续追踪 accuracy、precision、recall 和 F1 等指标。
-
生产部署需要具备若干关键能力。持久化存储用于跨会话保存对话历史,监控系统用于实时跟踪错误和延迟,而 staged rollout 则用于先在一部分流量上试运行改动。
-
通过 canary testing 的 staged rollout,可以在全面放量前先发现问题。
-
构建评估数据集时,应至少收集 100+ 组 query-answer 对,并标注正确/错误,同时包含边界情况和对抗样本(如 prompt injection、超出范围请求)。
-
使用 LangSmith 做评估时,可以上传数据集,让 agent 在所有样本上跑一遍,审查失败案例,再迭代优化 prompts / tools,提高得分。
-
监控生产指标时,应重点关注 error rate、P95 latency、每次查询的 token 消耗,以及工具调用成功率,并为异常设置告警。
-
要通过审批队列实现 human-in-the-loop,可在决策点暂停工作流,把 state 保存到 checkpoint 中,通知人工审核员,然后在审批通过或拒绝后继续执行。