黑马大模型RAG与Agent智能体实战教程LangChain提示词——52、Agent智能体——Agent项目中间件和Agent创建

教程:https://www.bilibili.com/video/BV1yjz5BLEoY

代码:https://github.com/shangxiang0907/HeiMa-AI-LLM-RAG-Agent-Dev

云开发平台:https://hzh.sealos.run

文章目录

Agent智能体-12、[Agent项目]中间件和Agent创建

解释

→提示词:我现在要实现@zhisaotong_agent/PROJECT_OVERVIEW.md 中的middleware.py,下面是一个已有实现,帮我指出它是否有什么不符合最佳实践的地方和不符合生产的地方。最后请帮我优化,但是我不希望影响外部交互

py 复制代码
from typing import Callable
from utils.prompt_loader import load_system_prompts, load_report_prompts
from langchain.agents import AgentState
from langchain.agents.middleware import wrap_tool_call, before_model, dynamic_prompt, ModelRequest
from langchain.tools.tool_node import ToolCallRequest
from langchain_core.messages import ToolMessage
from langgraph.runtime import Runtime
from langgraph.types import Command
from utils.logger_handler import logger


@wrap_tool_call
def monitor_tool(
        # 请求的数据封装
        request: ToolCallRequest,
        # 执行的函数本身
        handler: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:             # 工具执行的监控
    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

        return result
    except Exception as e:
        logger.error(f"工具{request.tool_call['name']}调用失败,原因:{str(e)}")
        raise e


@before_model
def log_before_model(
        state: AgentState,          # 整个Agent智能体中的状态记录
        runtime: Runtime,           # 记录了整个执行过程中的上下文信息
):         # 在模型执行前输出日志
    logger.info(f"[log_before_model]即将调用模型,带有{len(state['messages'])}条消息。")

    logger.debug(f"[log_before_model]{type(state['messages'][-1]).__name__} | {state['messages'][-1].content.strip()}")

    return None


@dynamic_prompt                 # 每一次在生成提示词之前,调用此函数
def report_prompt_switch(request: ModelRequest):     # 动态切换提示词
    is_report = request.runtime.context.get("report", False)
    if is_report:               # 是报告生成场景,返回报告生成提示词内容
        return load_report_prompts()

    return load_system_prompts()

←AI回复

你这版实现里不符合最佳实践 / 不够生产级的点

  • 日志可能泄露敏感信息 :直接 logger.info(... args ...) 会把 token、手机号、cookie、用户隐私、长文本(甚至知识库片段)完整打到日志里,生产上风险很高,也容易把日志打爆。
  • 异常处理不保留原始堆栈except Exception as e: ... raise e 会丢失部分 traceback 语义;生产上应使用 logger.exception(...) + raise 来保留完整堆栈。
  • 健壮性不足 :默认假设 request.tool_call['name'] / ['args'] 一定存在、state['messages'] 一定非空且最后一条一定有 content;实际运行时很容易在边界条件下抛 KeyError/IndexError/AttributeError,反而影响主流程。
  • logger 使用方式不一致 :项目里统一是 get_logger(__name__),你这版是从 utils.logger_handler import logger(在当前仓库结构里也不匹配),生产上会导致 logger 行为不一致或直接 import 失败。

我做的优化(不改变外部交互)

我已经新建并写入 zhisaotong_agent/agent/tools/middleware.py,保持:

  • 同样的三个入口monitor_toollog_before_modelreport_prompt_switch
  • 同样的装饰器与签名语义@wrap_tool_call / @before_model / @dynamic_prompt
  • 同样的外部行为 :遇到 fill_context_for_report 仍然设置 request.runtime.context["report"] = True,动态提示词切换逻辑不变

增强点:

  • 日志脱敏 + 截断 :对常见敏感字段做 ***REDACTED***,并限制日志长度,避免泄露与日志爆炸
  • 异常保留堆栈logger.exception(...) + raise
  • 边界条件更稳:空消息、无 content、tool_call 缺字段等都不会把主流程搞崩

文件位置:zhisaotong_agent/agent/tools/middleware.py(已通过本地 lint 检查无报错)。

→提示词:帮我添加自测代码

←AI回复

已添加自测代码(且不会影响外部交互)

我在 zhisaotong_agent/agent/tools/middleware.py 底部加了 if __name__ == "__main__":smoke test,主要覆盖:

  • 脱敏/截断逻辑 :直接运行 _safe_preview(),确保不会泄露 token 等字段
  • 装饰器兼容性 :由于 @wrap_tool_call/@before_model/@dynamic_prompt 会把函数变成框架中间件对象(运行时不可直接调用 ,且也没有稳定的 __wrapped__),自测会自动跳过直接调用,避免依赖框架内部实现细节导致误报

你可以在项目根目录运行:

bash 复制代码
python -m zhisaotong_agent.agent.tools.middleware

目前输出会确认 _safe_preview 正常,并提示装饰后的对象跳过调用测试(这是预期行为)。如果你希望"真正调用链路"的自测,我建议把它放到集成测试里(用实际的 LangGraph/Agent 运行一小段流程),而不是在模块 __main__ 里硬调用装饰对象。

代码

middleware.py

py 复制代码
"""
工具调用与模型调用相关的中间件(监控、日志、动态提示词切换等)。

约束:
- 对外交互保持稳定:函数名、装饰器、入参与返回值类型不变;
- 仅增强健壮性与生产可用性(日志安全、异常保留栈、边界条件处理)。
"""

from __future__ import annotations

from typing import Any, Callable, Mapping

from langchain.agents import AgentState
from langchain.agents.middleware import ModelRequest, before_model, dynamic_prompt, wrap_tool_call
from langchain.tools.tool_node import ToolCallRequest
from langchain_core.messages import ToolMessage
from langgraph.runtime import Runtime
from langgraph.types import Command

from zhisaotong_agent.utils.logger_handler import get_logger
from zhisaotong_agent.utils.prompt_loader import load_report_prompts, load_system_prompts

logger = get_logger(__name__)


def _redact_mapping(obj: Mapping[str, Any]) -> dict[str, Any]:
    """
    对常见敏感字段做脱敏,避免把密钥/令牌/隐私直接写入日志。
    仅用于日志展示,不影响工具真实入参。
    """

    sensitive_keys = {
        "password",
        "passwd",
        "secret",
        "token",
        "access_token",
        "api_key",
        "apikey",
        "authorization",
        "cookie",
        "session",
        "session_id",
        "phone",
        "mobile",
        "email",
        "id_card",
    }

    redacted: dict[str, Any] = {}
    for k, v in obj.items():
        key_lower = str(k).lower()
        if key_lower in sensitive_keys:
            redacted[k] = "***REDACTED***"
        else:
            redacted[k] = v
    return redacted


def _safe_preview(value: Any, *, max_len: int = 2000) -> str:
    """
    将任意对象转为可安全打印的短字符串,防止日志爆炸/序列化异常。
    """

    try:
        if isinstance(value, Mapping):
            value = _redact_mapping(value)  # type: ignore[assignment]
        text = repr(value)
    except Exception:
        text = "<unreprable>"
    if len(text) > max_len:
        return text[: max_len - 3] + "..."
    return text


@wrap_tool_call
def monitor_tool(
    request: ToolCallRequest,
    handler: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:
    """
    工具调用监控中间件。

    关于 @wrap_tool_call 装饰器(简化理解):
    - 在 LangGraph/Agent 执行链中,每当「需要调用某个工具」时,框架会把原本
      直接执行工具的步骤"包裹"成一个中间件调用;
    - 这个中间件的签名约定为 monitor_tool(request, handler):
        - request: ToolCallRequest,对应"这一次工具调用"的上下文(包括工具名、
          参数、runtime 等信息);
        - handler: 真正执行工具的函数(可以理解成"next" 或 "inner handler")。
    - monitor_tool 里可以:
        - 在调用前后打日志;
        - 修改 request 或 runtime.context;
        - 决定是否继续调用 handler,或者短路返回。

    本函数的核心逻辑是:
    - 从 request 中取出工具名称与参数,用安全方式打印日志(避免泄露敏感信息);
    - 调用 handler(request) 让真正的工具执行;
    - 如果是特定工具 fill_context_for_report,就在 runtime.context 中打一个标记,
      供后续 @dynamic_prompt 中间件判断是否切换到"报告场景"的提示词。
    """

    tool_call = getattr(request, "tool_call", None) or {}
    tool_name = tool_call.get("name", "<unknown>")
    tool_args = tool_call.get("args", None)

    # 生产最佳实践:避免直接打印完整参数(可能含敏感信息/超长文本)
    logger.info("[tool monitor]执行工具:%s", tool_name)
    logger.info("[tool monitor]传入参数:%s", _safe_preview(tool_args))

    try:
        result = handler(request)
        logger.info("[tool monitor]工具%s调用成功", tool_name)

        # 保持外部交互一致:仍然以同样 key 打标记
        if tool_name == "fill_context_for_report":
            # Runtime.context 通常是跨节点共享的 dict;这里仅设置布尔标志位
            request.runtime.context["report"] = True

        return result
    except Exception:
        # 使用 exception 记录堆栈,且用 bare raise 保留原始 traceback
        logger.exception("工具%s调用失败", tool_name)
        raise


@before_model
def log_before_model(state: AgentState, runtime: Runtime):
    """
    在模型执行前输出日志(消息条数与最新一条消息概览)的中间件。

    关于 @before_model 装饰器(简化理解):
    - 在一次 Agent / Graph 的循环里,当"上一个节点"已经准备好了 messages,
      即将进入「调用大模型」这一步时,框架会先依次执行所有 @before_model
      注册的中间件;
    - 这些中间件的签名固定为 log_before_model(state, runtime):
        - state: 当前 AgentState,里边最关键的是 state["messages"],包含了
          系统/用户/AI/工具消息等;
        - runtime: LangGraph Runtime,包含运行上下文、配置、链路信息等。

    本函数只做只读日志:
    - 统计消息条数,方便排查"重复追加历史消息"等问题;
    - 打印最后一条消息的类型与内容(经过 _safe_preview 截断),方便观测当前
      发给模型的最后一条输入长什么样;
    - 不修改 state 与 runtime,保证是"零副作用"的观测中间件。
    """

    messages = state.get("messages") if isinstance(state, dict) else None
    msg_count = len(messages) if isinstance(messages, list) else 0
    logger.info("[log_before_model]即将调用模型,带有%d条消息。", msg_count)

    if not messages:
        return None

    last = messages[-1]
    try:
        content = getattr(last, "content", None)
        content_text = content.strip() if isinstance(content, str) else _safe_preview(content)
        logger.debug("[log_before_model]%s | %s", type(last).__name__, content_text)
    except Exception:
        logger.debug("[log_before_model]%s | <unloggable message>", type(last).__name__)

    return None


@dynamic_prompt
def report_prompt_switch(request: ModelRequest):
    """
    动态切换提示词:当 runtime.context['report'] 为 True 时切换为报告提示词。

    关于 @dynamic_prompt 装饰器(简化理解):
    - 每次框架要"构造给模型的系统提示词 / 主提示词"时,会调用此函数来获取
      本次调用应该使用的 prompt 内容;
    - 传入的 request: ModelRequest,里面包含本次模型调用相关的上下文信息,
      包括 runtime.context(可在工具中间件里写入标志位)。

    本函数的核心逻辑:
    - 读取 runtime.context["report"] 这个布尔标记;
        - 该标记由 monitor_tool 在特定工具(fill_context_for_report)执行后设置;
    - 如果是报告场景(report=True),则加载报告专用提示词;
    - 否则回退到系统默认提示词。

    这样就把「是否进入报告生成场景」这个业务决策,从 prompt 模板里解耦出来,
    统一用 runtime.context 这个"执行上下文状态"进行传递。
    """

    is_report = bool(request.runtime.context.get("report", False))
    if is_report:
        return load_report_prompts()
    return load_system_prompts()


if __name__ == "__main__":
    """
    简单自测代码(smoke test):
    - 验证脱敏与日志预览函数不会抛异常;
    - 用最小 dummy 对象验证 report 标记与日志函数的健壮性;
    - 提示词加载依赖 config/prompts 配置文件,若缺失会打印友好错误。

    运行方式(在项目根目录):
        python -m zhisaotong_agent.agent.tools.middleware
    """

    class _DummyRuntime:
        def __init__(self):
            self.context: dict[str, Any] = {}

    class _DummyToolRequest:
        def __init__(self, name: str, args: Any):
            self.tool_call = {"name": name, "args": args}
            self.runtime = _DummyRuntime()

    class _DummyMsg:
        def __init__(self, content: Any):
            self.content = content

    print("== middleware smoke test ==")
    print("safe_preview:", _safe_preview({"token": "abc", "k": "v", "text": "x" * 10}))

    dummy_req = _DummyToolRequest(
        "fill_context_for_report",
        {"user_id": "1001", "token": "should-not-leak", "note": "x" * 5000},
    )

    def _dummy_handler(_req: Any):
        return ToolMessage(content="ok", tool_call_id="smoke_test")

    try:
        # 这些函数上方有 langchain/langgraph 的装饰器。
        # 装饰器可能会把"函数"替换成"中间件对象",导致它在运行时不再是可直接调用的普通函数。
        # 一种常见做法是通过 functools.wraps 保留原函数在 __wrapped__ 里;
        # 因此这里尽量尝试调用 __wrapped__,但前提是它确实存在且可调用。
        _monitor = getattr(monitor_tool, "__wrapped__", None)
        if callable(_monitor):
            _monitor(dummy_req, _dummy_handler)  # type: ignore[misc]
            print("monitor_tool(__wrapped__): ok; runtime.context['report'] =", dummy_req.runtime.context.get("report"))
        else:
            # 如果没有 __wrapped__,说明装饰器没有保留原函数,或装饰后对象不是可调用函数。
            # 这种情况下跳过调用测试,避免自测依赖框架内部实现细节而误报。
            print("monitor_tool: decorated object is not callable; skip call smoke test")
    except Exception as e:
        print("monitor_tool: failed:", e)

    try:
        _before = getattr(log_before_model, "__wrapped__", None)
        # 同上:尽量调用被装饰器包装前的原函数进行 smoke test。
        if callable(_before):
            _before({"messages": [_DummyMsg(" hello ")]}, _DummyRuntime())  # type: ignore[misc]
            print("log_before_model(__wrapped__): ok")
        else:
            # 保持自测稳健性:装饰器实现变化时不会导致自测崩溃。
            print("log_before_model: decorated object is not callable; skip call smoke test")
    except Exception as e:
        print("log_before_model: failed:", e)

    class _DummyModelRequest:
        def __init__(self, report_flag: bool):
            self.runtime = _DummyRuntime()
            self.runtime.context["report"] = report_flag

    for flag in (False, True):
        try:
            # report_prompt_switch 同样可能被框架装饰器包装为不可直接调用对象。
            _switch = getattr(report_prompt_switch, "__wrapped__", None)
            if callable(_switch):
                prompt = _switch(_DummyModelRequest(flag))  # type: ignore[misc]
                print(f"report_prompt_switch(__wrapped__)(report={flag}): ok; prompt_len={len(prompt)}")
            else:
                # 依赖最少:没有 __wrapped__ 就不调用,只提示跳过。
                print(f"report_prompt_switch: decorated object is not callable; skip call smoke test (report={flag})")
        except Exception as e:
            print(f"report_prompt_switch(report={flag}): failed:", e)

运行

bash 复制代码
python -m zhisaotong_agent.agent.tools.middleware

结果

复制代码
(project) devbox@devbox:~/project$ python -m zhisaotong_agent.agent.tools.middleware
== middleware smoke test ==
safe_preview: {'token': '***REDACTED***', 'k': 'v', 'text': 'xxxxxxxxxx'}
monitor_tool: decorated object is not callable; skip call smoke test
log_before_model: decorated object is not callable; skip call smoke test
report_prompt_switch: decorated object is not callable; skip call smoke test (report=False)
report_prompt_switch: decorated object is not callable; skip call smoke test (report=True)
相关推荐
国医中兴15 小时前
Flutter 三方库 langchain_google 的鸿蒙化适配指南 - 链接 Gemini 智慧中枢、LangChain AI 实战、鸿蒙级智能应用专家
flutter·langchain·harmonyos
鹭天16 小时前
Langchain学习笔记
笔记·学习·langchain
Timer@18 小时前
LangChain 教程 01|概述:AI 应用开发的新范式
人工智能·语言模型·langchain·前端框架
秀儿y1 天前
LangChain搭建简单本地知识库
langchain
虎头金猫1 天前
小米摄像头本地化存储教程:Go2RTC+EasyNVR 搭建私有监控系统
langchain·开源·github·aigc·智能家居·开源软件·ai编程
此生只爱蛋1 天前
【LangChain】示例选择器
langchain
前进的李工1 天前
LangChain使用之Model IO(提示词模版之FewShotPromptTemplate)
开发语言·人工智能·语言模型·langchain·agent
小仓桑1 天前
【Agent智能体项目实战五】LangChain 提示词模板(PromptTemplate)极简实战:两种调用方式详解
langchain·agent
小超同学你好2 天前
Langgraph 18. Skill 四种形态 —— Inline / File-based / External / Meta(含代码示例)
人工智能·语言模型·langchain