分类:AI编程
在演示场景中,Agent 常常被理解为“收到问题、调用一次模型、返回答案”。但进入真实业务后,开发者要面对日志、状态统计、上下文注入、请求拦截、工具管理和文件访问控制等问题。一篇关于 LangChain.js Agent 中间件的技术实践,把这些需求放进了同一个执行框架中:中间件不是挂在 Agent 外层的普通回调,而是参与 Agent 执行图的控制节点。
从单次模型调用到执行图
素材中的核心判断是,带工具的 Agent 不是一次模型请求,而是一段可循环的执行流程。一次完整调用会从 beforeAgent 开始,进入 beforeModel,再通过 wrapModelCall 调用模型;模型返回后触发 afterModel。如果模型要求调用工具,流程会进入 wrapToolCall 和工具执行,随后回到 beforeModel;如果模型不再请求工具,才会进入 afterAgent 并结束。
这一流程区分了两类钩子:
- beforeAgent 与 afterAgent 面向一次完整的 Agent 调用。
- beforeModel 与 afterModel 面向一次模型调用。只要模型在工具返回后再次被询问,模型级钩子就会再次执行。
wrapModelCall 与 wrapToolCall 则是包装器。它们会拿到一个 handler,只有调用 handler,后续中间件或真正的模型、工具才会继续执行。因此,这一层适合承担参数改写、日志、重试和结果包装等任务。示例使用 ESM 与 .mjs 文件,依赖包括 langchain ^1.5.15、@langchain/openai ^1.6.2、@langchain/langgraph ^1.4.21、zod ^4.6.5、deepagents ^1.14.2、dotenv ^18.0.6。素材提到,@langchain/openai 对 Node.js 版本要求较高,直接使用 Node.js 22 更省心。模型通过 ChatOpenAI 创建,可连接兼容 OpenAI 协议的服务,temperature 被设为 0,用于降低回答随机性,便于观察中间件是否按预期执行,但它不等于数学意义上的绝对确定。
生命周期、状态统计与请求拦截
示例首先给出一个日志中间件。它通过 createMiddleware 创建,并用 Zod 的 stateSchema 定义自定义状态字段,例如 modelCallCount,默认值为 0。这个字段不需要在调用 Agent 时手动传入,框架会完成初始化,生命周期钩子随后通过 state.modelCallCount 读取。
在 afterModel 阶段,中间件会读取最后一条模型消息,并生成最长 200 字符的预览。由于消息内容可能是字符串,也可能是结构化内容块,示例先判断类型再生成摘要,避免日志代码影响 Agent 主流程。钩子不直接修改 state,而是返回局部状态更新,例如把 modelCallCount 加 1,再由执行图合并到总状态。
这种统计放在 afterModel 而不是 afterAgent,是因为模型可能被调用多次。素材给出的典型路径是:用户消息先让模型决定调用工具,工具返回结果后,模型再生成最终回答。这种情况下,模型通常会被调用 2 次。
中间件也可以在模型真正接收请求前改写内容。示例中的 AddContextMiddleware 使用 wrapModelCall,在原系统消息后追加一句回答约束。它会保留原 request 中的模型、消息、工具和运行时状态等字段,只覆盖 systemMessage。相比把所有规则写死在基础系统提示词中,这种方式更适合处理不同请求、租户上下文或临时规则。
更强的控制是短路。示例中的拦截中间件在 beforeModel 阶段检查最后一条消息,如果发现文本包含 BLOCKED,就返回一条 AI 消息,并通过 canJumpTo 与 jumpTo 将流程跳转到 end。canJumpTo 声明这个钩子允许跳到的目标,jumpTo 在运行时真正选择目标。由于短路发生在 beforeModel,模型不会调用,afterModel 不会执行,afterAgent 也不会执行。示例输出显示,最终回复为“该请求已被 middleware 拦截,无法处理”,modelCallCount 为 0。基于 wrapModelCall 的上下文注入中间件也没有执行,因为请求没有进入模型节点。这也提醒开发者,必须执行的资源清理不能只依赖 afterAgent。
动态工具与文件系统权限入口
除了观察和拦截,中间件还可以携带工具。示例定义了一个名为 get_current_time 的工具,用于返回当前 UTC 时间的 ISO 8601 字符串。即使工具没有参数,仍然使用 z.object({}) 明确声明一个空对象 Schema。工具名和描述会提供给模型,模型据此判断什么时候调用它。
素材还提到,示例将 DeepAgents 的文件系统中间件纳入后续内容。现有素材没有展开具体权限规则,但依赖中包含 deepagents ^1.14.2,且文章标题明确提到文件系统权限。这意味着,文件访问控制也可以被放进同一套中间件机制中理解:当 Agent 需要读取或写入文件时,权限判断可以作为执行图中的一个控制点,而不是事后补丁。
Agent 工程化的控制面价值
这组示例的意义,不只是展示几个 API,而是把 Agent 开发中常见的控制需求集中到一起。生命周期钩子描述执行顺序,stateSchema 提供可累计的状态字段,wrapModelCall 提供请求改写能力,beforeModel 的短路机制提供安全边界,动态工具则让能力注册与调用链路保持连接。
对开发者来说,这种设计可以减少把业务规则硬编码进系统提示词或工具函数内部的压力。不同请求可以在进入模型前被注入不同上下文,敏感内容可以在模型调用前被拦截,模型调用次数可以在状态中被记录。对于需要操作文件或外部工具的 Agent,权限控制也更容易被纳入统一流程。Agent 正在从“会调用模型的脚本”转向具备执行图、状态、拦截器和工具管理能力的软件系统,而中间件正是这类系统里的工程化控制面。
原创文章,作者:点点,如若转载,请注明出处:https://www.dian8dian.com/ai-agent-gong-cheng-hua-de-kong-zhi-mian-langchain-js-zhong