🎯 课程主题
深入学习装饰器参数 can_jump_to,掌握如何在钩子函数中动态改变 Agent 的执行轨迹,实现流程跳转控制。
📝 核心知识点
1. can_jump_to 参数概述
四个 Node-style 钩子函数可以额外接收 can_jump_to 参数,用于改变 Agent 的正常运行轨迹。
| 值 | 跳转目标 | 说明 |
|---|---|---|
"end" | 流程结尾 | 直接结束 Agent(若有 after_agent 钩子仍会执行) |
"tools" | 工具节点 | 跳转到指定工具节点(若有 before_model 则仍会执行) |
"model" | 模型节点 | 跳转到模型节点重新调用(若有 before_model 则仍会执行) |
2. 为什么需要流程跳转?
- 上下文窗口溢出:检测到 Token 超限,直接
can_jump_to="end"终止执行 - 跳过模型调用:已知条件可直接伪造 AIMessage,跳到工具节点节省成本
- 二次尝试:模型输出不满意时,补充 SystemMessage 后
can_jump_to="model"重新调用
3. 装饰器方式 vs 类方式使用 can_jump_to
| 方式 | 用法 |
|---|---|
| 装饰器 | 直接在装饰器中传入参数,如 @before_model(can_jump_to="end") |
| 类 | 使用 @hook_configure(can_jump_to="...") 装饰对应方法 |
4. 类方式中多钩子合并注意事项
类中所有 before_model 方法必须合并为一个,通过分支逻辑区分不同跳转目标(因为类中只有一个 before_model 方法名可用)。
🏗️ 架构与工作流
用户输入
│
▼
before_model(can_jump_to="tools") ──→ 跳过模型,直接到工具
│
▼
[模型调用]
│
▼
after_model(can_jump_to="model") ──→ 不满意,重新调用模型
│
▼
[模型再调用]
│
▼
before_model(can_jump_to="end") ──→ 溢出检测,直接结束
│
▼
after_agent → 流程结束
💻 代码实战
2.1 基于装饰器的实现
from langchain.agents import create_agent
from langchain.agents.middleware import (
before_model, after_model
)
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_core.tools import tool
# ---- 定义工具 ----
@tool
def get_news(query: str) -> str:
"""查询新闻"""
return f"关于 {query} 的最新新闻:今日AI领域有重大突破..."
# ---- 钩子函数1: 伪造 AIMessage,跳过模型调用直接到工具 ----
@before_model(can_jump_to="tools")
def fake_ai_message(state, runtime):
"""检测到 direct_to_tools 字段时,伪造 AIMessage 跳过模型调用"""
messages = state["messages"]
last_msg = messages[-1]
if hasattr(last_msg, "content") and "direct_to_tools" in last_msg.content:
# 伪造一条 AIMessage,包含工具调用
return {
"messages": [AIMessage(
content="",
tool_calls=[{
"name": "get_news",
"args": {"query": "AI新闻"},
"id": "call_001"
}]
)]
}
return None
# ---- 钩子函数2: 模型调用后不满意,强制重新调用 ----
@after_model(can_jump_to="model")
def retry_model(state, runtime):
"""检测到 retry_model 字段且未重试过时,追加 SystemMessage 重新调用"""
messages = state["messages"]
# 检查用户原始输入是否包含 retry_model
user_msgs = [m for m in messages if isinstance(m, HumanMessage)]
if not user_msgs:
return None
last_user = user_msgs[-1]
if "retry_model" not in last_user.content:
return None
# 检查是否已经重试过(避免死循环)
ai_msgs = [m for m in messages if isinstance(m, AIMessage)]
if ai_msgs and "二次回答" in ai_msgs[-1].content:
return None # 已经重试过,不再重复
# 追加 SystemMessage 要求重试
return {
"messages": [SystemMessage(
content="你必须以'二次回答'开头,并且只用一句话回答。"
)]
}
# ---- 钩子函数3: 溢出检测,直接结束 ----
@before_model(can_jump_to="end")
def check_overflow(state, runtime):
"""检测到 overflow 字段时,提前终止 Agent"""
messages = state["messages"]
last_msg = messages[-1]
if hasattr(last_msg, "content") and "overflow" in last_msg.content:
return {
"messages": [AIMessage(content="上下文窗口溢出,已终止执行。")]
}
return None
# ---- 创建 Agent ----
agent = create_agent(
model=model,
tools=[get_news],
middleware=[
fake_ai_message, # before_model: 跳 tools
retry_model, # after_model: 跳 model
check_overflow # before_model: 跳 end
]
)
# ---- 测试用例 ----
def test(query: str):
print(f"\n{'='*50}")
print(f"输入: {query}")
response = agent.invoke({"messages": [HumanMessage(content=query)]})
for msg in response["messages"]:
msg.pretty_print()
# Case 1: 跳过模型,直接到工具
test("请帮我查询今日天气 direct_to_tools")
# Case 2: 模型调用后重试
test("请随便介绍一下LangChain retry_model")
# Case 3: 溢出直接结束
test("你好 overflow")
# Case 4: 正常执行
test("今天有什么新闻?")
2.2 基于类的实现(使用 @hook_configure)
from langchain.agents.middleware import AgentMiddleware, hook_configure
class MyMiddleware(AgentMiddleware):
@hook_configure(can_jump_to="tools")
@hook_configure(can_jump_to="end")
def before_model(self, state, runtime):
"""合并两个 before_model 跳转逻辑"""
messages = state["messages"]
last_msg = messages[-1]
content = last_msg.content if hasattr(last_msg, "content") else ""
# 分支1: 跳转到工具
if "direct_to_tools" in content:
return {
"messages": [AIMessage(
content="",
tool_calls=[{
"name": "get_news",
"args": {"query": "AI新闻"},
"id": "call_001"
}]
)]
}
# 分支2: 溢出直接结束
if "overflow" in content:
return {
"messages": [AIMessage(content="上下文窗口溢出,已终止执行。")]
}
return None
@hook_configure(can_jump_to="model")
def after_model(self, state, runtime):
"""二次重试逻辑"""
messages = state["messages"]
user_msgs = [m for m in messages if isinstance(m, HumanMessage)]
if not user_msgs:
return None
last_user = user_msgs[-1]
if "retry_model" not in last_user.content:
return None
ai_msgs = [m for m in messages if isinstance(m, AIMessage)]
if ai_msgs and "二次回答" in ai_msgs[-1].content:
return None
return {
"messages": [SystemMessage(
content="你必须以'二次回答'开头,并且只用一句话回答。"
)]
}
# 使用
agent = create_agent(
model=model,
tools=[get_news],
middleware=[MyMiddleware()]
)
⚠️ 常见问题与避坑指南
| 问题 | 说明 |
|---|---|
| 死循环 | can_jump_to="model" 会导致模型重新调用,务必加终止条件(如标记已重试过的标志) |
| 类方式钩子名冲突 | 类中只有一个 before_model 方法名,多个装饰器的 before_model 钩子必须合并到同一个方法中 |
| 跳转后钩子仍执行 | can_jump_to="model" 跳转后,如果目标节点前有 before_model 钩子,它仍会执行;同理 "end" 后 after_agent 也会执行 |
| 忘记 return 跳转信息 | 仅仅设置 can_jump_to 参数不会自动跳转,还需要在函数体内满足条件时返回相应数据 |
💡 个人总结与延伸
can_jump_to赋予了中间件流程控制能力,从单纯的 Observe/Mutate 扩展到了 Flow Control- 典型应用:成本优化(伪造 AIMessage 跳过模型调用)、质量保障(二次调用优化输出)、安全兜底(溢出即终止)
- 与 LangGraph 原生的
Command(goto=...)机制一脉相承,是高层 Agent 框架对底层图控制的封装 - 类方式中使用
@hook_configure虽可行,但多个跳转目标合并到一个方法中会降低可读性——这也是单钩子优先用装饰器的原因之一