05-Agent 智能体开发实战指南(五):中间件系统与动态提示词

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

九、本章小结

核心要点

  1. 中间件类型

    • 节点式:before_agent, after_agent, before_model, after_model
    • 包装式:wrap_tool_call, wrap_model_call
    • 特殊:dynamic_prompt
  2. 典型应用

    • 工具调用监控
    • 动态提示词切换
    • 安全防护
    • 性能监控
    • 缓存机制
  3. 设计原则

    • 单一职责:每个中间件只做一件事
    • 无副作用:尽量不修改状态
    • 异常处理:捕获并记录异常

下章预告

下一篇我们将讲解 RAG 与向量存储,学习:

  • Chroma 向量数据库集成
  • 文本分片策略
  • MD5 去重机制
  • 检索器优化

  1. Agent 智能体开发实战指南(一):从 LLM 到 Agent 的认知升级
  2. Agent 智能体开发实战指南(二):工具调用系统深度解析
  3. Agent 智能体开发实战指南(三):ReAct 框架深度解析
  4. Agent 智能体开发实战指南(四):流式输出与状态管理
  5. Agent 智能体开发实战指南(五):中间件系统与动态提示词
  6. Agent 智能体开发实战指南(六):RAG 与向量存储实战
  7. Agent 智能体开发实战指南(七):项目架构设计与工程化实践
  8. Agent 智能体开发实战指南(八):UI 集成与生产部署

本文是《Agent 智能体开发实战指南》系列的第五篇,下一篇将深入讲解 RAG 与向量存储。

相关推荐
进击的雷神1 小时前
AJAX动态参数反爬、HTML嵌套网站提取、UPSERT增量更新、空值智能处理——沙特塑料展爬虫四大技术难关攻克纪实
爬虫·python·ajax·html
TEC_INO1 小时前
Linux45:RV1126+OPENCV对视频流进行视频腐蚀操作
人工智能·opencv·音视频
zhangshuang-peta1 小时前
MCP 中的“人机协同”:通过监督与政策保障自主人工智能的安全
人工智能·安全·ai agent·mcp·peta
RuiBo_Qiu1 小时前
【LLM进阶-Agent】11. MemGPT 介绍
人工智能·ai-native
2401_883035462 小时前
持续集成/持续部署(CI/CD) for Python
jvm·数据库·python
JobDocLS2 小时前
rknn3588的用法
python
TG_yunshuguoji2 小时前
阿里云代理商:阿里云部署OpenClaw 一键更新升级指南
服务器·人工智能·阿里云·云计算
郝学胜-神的一滴2 小时前
深度学习框架新纪元:PyTorch核心原理与工程实践全解析
人工智能·pytorch·python·深度学习·机器学习
禹笑笑-AI食用指南2 小时前
分享一个 OpenClaw 协同平台+CLI+工具调用思路+实战!
人工智能·ai·openclaw·龙虾