LangGraph-handler

LangGraph 中间件中的 Handler 深度实践

结合生产项目,聊聊中间件的洋葱模型与 6 种实用模式

项目背景

我们构建了一个 AI 测试用例生成系统,基于 LangGraph + DeepAgent 框架。核心链路如下:

前端 → LangGraph API → Agent → 中间件链 → Tools → LLM

核心原理:洋葱模型

项目定义了 4 个中间件,按顺序嵌套执行:

agent = create_agent(
    model=llm,
    tools=get_all_tools(),
    middleware=[
        skills_middleware,           # 1. 加载 Skills
        dynamic_model_selection,     # 2. 动态模型选择
        RAGMiddleware(),             # 3. RAG 工具控制
        PDFContextMiddleware()       # 4. PDF 处理
    ],
    system_prompt=SYSTEM_PROMPT
)

执行流程(洋葱模型):

请求进入
    │
    ▼
┌─────────────────────────┐
│ 1. SkillsMiddleware     │
│    加载 Skills → System Prompt
└─────────────────────────┘
    │
    ▼
┌─────────────────────────┐
│ 2. dynamic_model_selection
│    检测图片 → 切换模型   │
└─────────────────────────┘
    │
    ▼
┌─────────────────────────┐
│ 3. RAGMiddleware        │
│    开关控制 → 过滤 RAG 工具
└─────────────────────────┘
    │
    ▼
┌─────────────────────────┐
│ 4. PDFContextMiddleware │
│    提取 PDF → 追加提示   │
└─────────────────────────┘
    │
    ▼
    LLM 推理(最内层)

每一层都可以在调用 handler 前后添加逻辑,这就是中间件的精髓。


6 种实战模式

模式1:标准调用(观察模式)

场景:日志、监控、条件开关。
示例RAGMiddleware 控制 RAG 功能是否启用。

async def awrap_model_call(self, request, handler):
    if self._is_rag_enabled(request):
        request = self._inject_rag_prompt(request)
    else:
        request = self._filter_rag_tools(request)
    return await handler(request)   # 继续向下传递

模式2:动态模型选择(请求修改)

场景:根据输入内容切换不同模型(成本优化)。

项目中,检测到图片消息时自动切换到多模态模型(豆包 Vision),纯文本则使用 DeepSeek,节省约 80% 成本

@wrap_model_call
async def dynamic_model_selection(request, handler):
    if _has_image_in_messages(request):
        model = image_llm_model      # 豆包 Vision(贵)
    else:
        model = deepseek_model       # DeepSeek(便宜)
    
    return await handler(request.override(model=model))

图片检测逻辑:

def _has_image_in_messages(request):
    for msg in request.messages:
        content = msg.content
        if isinstance(content, list):
            for block in content:
                if block.get("type") in ("image", "image_url"):
                    return True
    return False

模式3:渐进式工具暴露(项目核心创新 ⭐)

场景:按需暴露工具,避免 Agent 误调用,减少 token 消耗。

我们注册了 6 个工具(Excel、PDF、RAG 检索等)。当 RAG 功能关闭时,直接将 RAG 相关工具从列表中过滤掉:

class RAGMiddleware(AgentMiddleware):
    async def awrap_model_call(self, request, handler):
        if self._is_rag_enabled(request):
            return await handler(self._inject_rag_prompt(request))
        else:
            # 核心:过滤 RAG 工具
            return await handler(self._filter_rag_tools(request))
    
    def _filter_rag_tools(self, request):
        filtered = [t for t in request.tools if not self._is_rag_tool(t)]
        return request.override(tools=filtered)

效果

  • 启用 RAG → 工具列表包含 rag_queryrag_graph_search
  • 禁用 RAG → 仅保留 export_excelextract_pdf 等基础工具

模式4:多重调用(重试机制)

场景:HTTP 请求自动重试 + 指数退避。
项目中的 RAG 服务客户端实现:

async def _request(self, method, path, **kwargs):
    for attempt in range(3):   # 最多重试3次
        try:
            resp = await client.request(...)
            return resp.json()
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 401:   # token 过期
                await self._refresh_token()
                continue
        except (RequestError, TimeoutException):
            pass
        
        await asyncio.sleep(0.5 * (2 ** attempt))   # 0.5s, 1s, 2s
    raise RAGAPIError("请求失败")

模式5:响应修改

场景:在模型返回后修改输出(如格式化、敏感词过滤)。
项目中的 PDFContextMiddleware前置阶段注入 PDF 信息,也可扩展后置逻辑:

async def awrap_model_call(self, request, handler):
    # 前置:处理 PDF,修改请求
    if pdf_info:
        request = self._append_pdf_hint(request, pdf_info)
    
    response = await handler(request)   # 调用模型
    
    # 后置:修改响应(可选)
    response = self._filter_sensitive_words(response)
    return response

模式6:模型降级(Fallback)

场景:主模型限流或出错时自动降级。可在 dynamic_model_selection 中扩展:

@wrap_model_call
async def model_with_fallback(request, handler):
    try:
        return await handler(request)
    except RateLimitError:
        # 降级到备用模型
        return await handler(request.override(model=backup_model))
    except Exception:
        return await handler(request.override(model=free_model))

面试常见问答

Q:为什么这样设计中间件?
A:解决三个实际问题:

  • 图片和文本成本差异 → 动态模型选择
  • 无需 RAG 时仍暴露工具 → 渐进式工具过滤
  • 用户上传 PDF 需要解析 → PDF 中间件

Q:中间件顺序重要吗?
A:非常重要。

  1. SkillsMiddleware(加载技能指令)
  2. DynamicModelSelection(确定模型)
  3. RAGMiddleware(处理 RAG 工具)
  4. PDFContextMiddleware(处理附件)

顺序决定了数据的流动方向,错误顺序会导致功能失效。


总结

模式 项目应用 核心作用
标准调用 RAGMiddleware 条件开关
请求修改 dynamic_model_selection 动态切换模型
渐进式工具暴露 RAGMiddleware._filter_rag_tools 按需加载工具
多重调用 RAGServiceClient._request HTTP 重试
响应修改 PDFContextMiddleware 后置格式化
模型降级 可扩展 容错处理

核心思想:通过 handler 获得执行流程的控制权,将非功能性需求(开关、过滤、重试、降级)与业务逻辑解耦。

posted @ 2026-06-10 13:45  菠萝包与冰美式  阅读(24)  评论(0)    收藏  举报