Agent 智能体开发实战指南(五):中间件系统与动态提示词
系列导读:这是《Agent 智能体开发实战指南》系列的第五篇,将深入讲解 LangChain Agent 的中间件系统,包括 Hooks 机制、工具调用监控、动态提示词切换等高级功能,让你能够完全掌控 Agent 的执行流程。
一、什么是中间件?
1.1 核心概念
中间件(Middleware):在 Agent 执行过程中拦截和修改行为的组件。
类比理解:
用户请求 → [中间件 1] → [中间件 2] → [Agent] → [中间件 3] → 响应
↓ ↓ ↓ ↓
日志记录 权限检查 执行逻辑 结果格式化
1.2 为什么需要中间件?
| 场景 | 无中间件 | 有中间件 |
|---|---|---|
| 日志记录 | 在每个工具中写日志 | 一个中间件统一处理 |
| 权限检查 | 每个工具单独检查 | 统一拦截验证 |
| 提示词切换 | 硬编码在 Agent 中 | 动态注入上下文 |
| 错误处理 | 分散在各处 | 统一捕获处理 |
| 性能监控 | 手动埋点 | 自动拦截统计 |
1.3 中间件的价值
- 解耦:业务逻辑与横切关注点分离
- 复用:一个中间件可用于多个 Agent
- 灵活:运行时动态添加/移除
- 可观测:统一监控和调试
二、Hooks 机制详解
2.1 六类钩子函数
LangChain 提供两类钩子:
节点式钩子(执行点顺序拦截):
| 钩子 | 触发时机 | 用途 |
|---|---|---|
before_agent |
Agent 执行前 | 预处理、权限检查 |
after_agent |
Agent 执行后 | 后处理、结果格式化 |
before_model |
模型调用前 | 修改输入、注入上下文 |
after_model |
模型调用后 | 修改输出、日志记录 |
包装式钩子(针对工具和模型):
| 钩子 | 触发时机 | 用途 |
|---|---|---|
wrap_tool_call |
工具调用时 | 监控、重试、日志 |
wrap_model_call |
模型调用时 | 修改模型行为 |
2.2 钩子执行顺序
用户请求
↓
┌─────────────────┐
│ before_agent │ ← 第 1 步:Agent 执行前
└────────┬────────┘
↓
┌─────────────────┐
│ before_model │ ← 第 2 步:模型调用前
└────────┬────────┘
↓
┌─────────────────┐
│ wrap_model_call│ ← 第 3 步:包装模型调用
└────────┬────────┘
↓
┌─────────────────┐
│ Model 执行 │
└────────┬────────┘
↓
┌─────────────────┐
│ after_model │ ← 第 4 步:模型调用后
└────────┬────────┘
↓
┌─────────────────┐
│ wrap_tool_call │ ← 第 5 步:工具调用(如有)
└────────┬────────┘
↓
┌─────────────────┐
│ after_agent │ ← 第 6 步:Agent 执行后
└────────┬────────┘
↓
返回响应
三、实战:工具调用监控
3.1 使用 @wrap_tool_call
python
from typing import Callable
from langchain.agents.middleware import wrap_tool_call
from langchain.tools.tool_node import ToolCallRequest
from langchain_core.messages import ToolMessage
from utils.logger_handler import logger
@wrap_tool_call
def monitor_tool(
request: ToolCallRequest, # 请求数据封装
handler: Callable[[ToolCallRequest], ToolMessage], # 执行的函数本身
) -> ToolMessage:
"""工具执行监控中间件"""
# 执行前:记录日志
logger.info(f"[tool monitor] 执行工具:{request.tool_call['name']}")
logger.info(f"[tool monitor] 传入参数:{request.tool_call['args']}")
try:
# 执行工具
result = handler(request)
# 执行后:记录成功日志
logger.info(f"[tool monitor] 工具{request.tool_call['name']}调用成功")
# 特殊处理:检测到报告生成工具,标记上下文
if request.tool_call['name'] == "fill_context_for_report":
request.runtime.context["report"] = True
logger.info("[tool monitor] 检测到报告生成场景,已标记上下文")
return result
except Exception as e:
# 异常处理
logger.error(f"工具{request.tool_call['name']}调用失败,原因:{str(e)}")
raise e
3.2 关键参数解析
ToolCallRequest 结构:
python
ToolCallRequest(
tool_call={
"name": "get_weather", # 工具名
"args": {"city": "深圳"}, # 参数
"id": "call_abc123" # 调用 ID
},
runtime=Runtime(
context={"report": False} # 运行时上下文
)
)
handler 函数:
- 实际执行工具的函数
- 必须调用它,否则工具不会执行
- 可以修改参数或结果
3.3 应用场景
1. 日志记录:
python
@wrap_tool_call
def log_tool_call(request, handler):
start_time = time.time()
result = handler(request)
duration = time.time() - start_time
logger.info(f"工具{request.tool_call['name']}耗时:{duration:.2f}s")
return result
2. 重试机制:
python
@wrap_tool_call
def retry_tool_call(request, handler):
max_retries = 3
for i in range(max_retries):
try:
return handler(request)
except Exception as e:
if i == max_retries - 1:
raise
logger.warning(f"工具调用失败,重试 {i+1}/{max_retries}")
time.sleep(1)
3. 权限检查:
python
@wrap_tool_call
def auth_tool_call(request, handler):
user_id = request.runtime.context.get("user_id")
tool_name = request.tool_call['name']
if not has_permission(user_id, tool_name):
raise PermissionError(f"用户{user_id}无权调用工具{tool_name}")
return handler(request)
四、实战:模型调用前日志
4.1 使用 @before_model
python
from langchain.agents.middleware import before_model
from langchain.agents import AgentState
from langgraph.runtime import Runtime
from utils.logger_handler import logger
@before_model
def log_before_model(
state: AgentState, # 整个 Agent 的状态记录
runtime: Runtime, # 执行过程中的上下文信息
):
"""在模型执行前输出日志"""
logger.info(f"[log_before_model] 即将调用模型,带有{len(state['messages'])}条消息。")
# 获取最新消息
latest_message = state['messages'][-1]
logger.debug(
f"[log_before_model] {type(latest_message).__name__} | "
f"{latest_message.content.strip()[:100]}"
)
# 返回 None 表示不修改状态
return None
4.2 修改状态
python
@before_model
def inject_context(state, runtime):
"""在模型调用前注入上下文信息"""
# 添加系统提示
state['messages'].insert(
0,
SystemMessage(content="你是一个专业的客服助手。")
)
# 返回修改后的状态(或不返回表示不修改)
return state
4.3 访问运行时上下文
python
@before_model
def check_report_mode(state, runtime):
"""检查是否为报告生成模式"""
is_report = runtime.context.get("report", False)
if is_report:
logger.info("当前为报告生成场景")
# 可以修改 state 或 runtime
else:
logger.info("当前为普通问答场景")
return None
五、实战:动态提示词切换
5.1 场景说明
需求:同一个 Agent,在不同场景下使用不同的 system_prompt
- 普通问答场景:使用客服提示词
- 报告生成场景:使用报告生成提示词
传统做法:创建两个 Agent
python
normal_agent = create_agent(model, tools, system_prompt=normal_prompt)
report_agent = create_agent(model, tools, system_prompt=report_prompt)
问题:
- 代码重复
- 维护成本高
- 无法运行时切换
中间件方案:一个 Agent,动态切换提示词
5.2 使用 @dynamic_prompt
python
from langchain.agents.middleware import dynamic_prompt, ModelRequest
from utils.prompt_loader import load_system_prompts, load_report_prompts
@dynamic_prompt
def report_prompt_switch(request: ModelRequest):
"""动态切换提示词"""
# 从运行时上下文获取标记
is_report = request.runtime.context.get("report", False)
if is_report:
# 报告生成场景,返回报告提示词
logger.info("使用报告生成提示词")
return load_report_prompts()
# 默认场景,返回系统提示词
logger.info("使用系统提示词")
return load_system_prompts()
5.3 完整流程
1. 用户提问:"给我生成我的使用报告"
↓
2. Agent 分析:需要生成报告
↓
3. 调用 fill_context_for_report 工具
↓
4. monitor_tool 中间件检测到,设置 runtime.context["report"] = True
↓
5. 调用模型前,dynamic_prompt 中间件被触发
↓
6. report_prompt_switch 读取 context["report"] = True
↓
7. 返回报告生成提示词,注入模型调用
↓
8. 模型根据报告提示词生成报告
5.4 标记工具设计
python
@tool(description="无入参,无返回值,调用后触发中间件自动为报告生成的场景动态注入上下文信息,为后续提示词切换提供上下文信息")
def fill_context_for_report():
"""标记工具:仅用于触发中间件,不执行实际逻辑"""
return "fill_context_for_report 已调用"
设计要点:
- 无实际业务逻辑
- 仅作为"信号"触发中间件
- description 清晰说明用途
六、中间件组合使用
6.1 注册多个中间件
python
from langchain.agents import create_agent
agent = create_agent(
model=chat_model,
system_prompt=load_system_prompts(),
tools=[...],
middleware=[
monitor_tool, # 工具监控
log_before_model, # 模型调用日志
report_prompt_switch, # 动态提示词
],
)
6.2 中间件执行顺序
python
# 中间件按注册顺序执行
middleware=[
middleware_a, # 第 1 个执行
middleware_b, # 第 2 个执行
middleware_c, # 第 3 个执行
]
注意:某些钩子的执行顺序可能受 LangChain 内部逻辑影响,建议通过日志验证。
6.3 条件启用中间件
python
def create_agent_with_middleware(scenario="normal"):
base_middleware = [monitor_tool, log_before_model]
# 根据场景添加特定中间件
if scenario == "report":
base_middleware.append(report_prompt_switch)
elif scenario == "debug":
base_middleware.append(debug_logger)
return create_agent(
model=chat_model,
tools=tools,
middleware=base_middleware,
)
七、高级应用
7.1 安全防护
python
@wrap_tool_call
def security_check(request, handler):
"""安全检查中间件"""
# 检查敏感工具
sensitive_tools = ["delete_user", "export_data"]
if request.tool_call['name'] in sensitive_tools:
user_id = request.runtime.context.get("user_id")
if not is_admin(user_id):
logger.warning(f"非管理员用户{user_id}尝试调用敏感工具")
raise PermissionError("无权执行此操作")
# 检查参数中的敏感信息
args = request.tool_call.get('args', {})
if contains_sensitive_data(args):
logger.warning("检测到敏感参数")
# 可以脱敏或拒绝
return handler(request)
7.2 性能监控
python
import time
from collections import defaultdict
tool_stats = defaultdict(lambda: {"count": 0, "total_time": 0})
@wrap_tool_call
def performance_monitor(request, handler):
"""性能监控中间件"""
start = time.time()
result = handler(request)
duration = time.time() - start
# 统计
tool_name = request.tool_call['name']
tool_stats[tool_name]["count"] += 1
tool_stats[tool_name]["total_time"] += duration
logger.info(f"工具{tool_name}耗时:{duration:.3f}s")
return result
# 定期输出统计
def print_stats():
for tool, stats in tool_stats.items():
avg_time = stats["total_time"] / stats["count"]
print(f"{tool}: 调用{stats['count']}次,平均耗时{avg_time:.3f}s")
7.3 缓存机制
python
from functools import lru_cache
cache = {}
@wrap_tool_call
def cache_tool_call(request, handler):
"""工具调用缓存中间件"""
tool_name = request.tool_call['name']
args = str(request.tool_call.get('args', {}))
cache_key = f"{tool_name}:{args}"
# 检查缓存
if cache_key in cache:
logger.info(f"命中缓存:{cache_key}")
return ToolMessage(content=cache[cache_key])
# 执行工具
result = handler(request)
# 写入缓存(只缓存成功结果)
cache[cache_key] = result.content
return result
八、调试技巧
8.1 中间件链路日志
python
@before_agent
def log_entry(state, runtime):
logger.info("=== Agent 执行开始 ===")
return None
@after_agent
def log_exit(state, runtime):
logger.info("=== Agent 执行结束 ===")
return None
@wrap_tool_call
def log_tool(request, handler):
logger.info(f"→ 工具调用:{request.tool_call['name']}")
result = handler(request)
logger.info(f"← 工具返回:{result.content[:50]}")
return result
8.2 上下文追踪
python
@wrap_tool_call
def trace_context(request, handler):
"""追踪运行时上下文变化"""
before_context = dict(request.runtime.context)
result = handler(request)
after_context = dict(request.runtime.context)
# 检测上下文变化
changes = set(after_context.keys()) - set(before_context.keys())
if changes:
logger.info(f"上下文新增键:{changes}")
return result
九、本章小结
核心要点
-
中间件类型:
- 节点式:before_agent, after_agent, before_model, after_model
- 包装式:wrap_tool_call, wrap_model_call
- 特殊:dynamic_prompt
-
典型应用:
- 工具调用监控
- 动态提示词切换
- 安全防护
- 性能监控
- 缓存机制
-
设计原则:
- 单一职责:每个中间件只做一件事
- 无副作用:尽量不修改状态
- 异常处理:捕获并记录异常
下章预告
下一篇我们将讲解 RAG 与向量存储,学习:
- Chroma 向量数据库集成
- 文本分片策略
- MD5 去重机制
- 检索器优化
- Agent 智能体开发实战指南(一):从 LLM 到 Agent 的认知升级
- Agent 智能体开发实战指南(二):工具调用系统深度解析
- Agent 智能体开发实战指南(三):ReAct 框架深度解析
- Agent 智能体开发实战指南(四):流式输出与状态管理
- Agent 智能体开发实战指南(五):中间件系统与动态提示词
- Agent 智能体开发实战指南(六):RAG 与向量存储实战
- Agent 智能体开发实战指南(七):项目架构设计与工程化实践
- Agent 智能体开发实战指南(八):UI 集成与生产部署
本文是《Agent 智能体开发实战指南》系列的第五篇,下一篇将深入讲解 RAG 与向量存储。