LangChain.js Agent 中间件实战:生命周期、动态工具与文件权限

LangChain.js Agent 中间件实战:从生命周期拦截、动态工具到文件系统权限

当 Agent 只负责"收到问题,然后调用一次模型"时,代码通常很简单。但只要进入真实业务,问题很快就会出现:

  • 怎样统计模型到底调用了几次?
  • 怎样在不改业务 Prompt 的前提下动态补充系统指令?
  • 怎样在请求触达模型之前拦截敏感内容?
  • 工具能否由中间件提供,而不是全部堆在 Agent 的顶层配置里?
  • 工具执行后,怎样同时修改消息历史和自定义状态?
  • 给 Agent 文件读写能力时,怎样限制它只能在指定目录活动,并且只能修改指定文件?

这些问题都指向同一个概念:中间件不是 Agent 外面的一层普通回调,而是 Agent 执行图的一部分。

本文用一组可以串起来运行的 Node.js 示例,依次讲清生命周期钩子、模型与工具包装器、自定义状态、流程短路,以及 DeepAgents 的文件系统中间件。

一、准备运行环境

示例使用 ESM,因此代码文件使用 .mjs 后缀。依赖版本如下:

json 复制代码
{
  "dependencies": {
    "@langchain/langgraph": "^1.4.21",
    "@langchain/openai": "^1.6.2",
    "deepagents": "^1.14.2",
    "dotenv": "^18.0.6",
    "langchain": "^1.5.15",
    "zod": "^4.6.5"
  }
}

这一组依赖中,@langchain/openai 对 Node.js 版本的要求较高,直接使用 Node.js 22 更省心。安装依赖:

bash 复制代码
pnpm install

环境变量可以按下面的形式准备:

dotenv 复制代码
# 可选:需要 LangSmith 链路追踪时再配置
LANGCHAIN_API_KEY=your_langsmith_api_key
LANGCHAIN_PROJECT=your_project_name
LANGSMITH_TRACING=true

# 模型配置
MODEL_NAME=your_model_name
OPENAI_API_KEY=your_api_key
OPENAI_BASE_URL=your_openai_compatible_base_url

所有示例都通过同一种方式创建模型:

js 复制代码
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
  temperature: 0,
});

import "dotenv/config" 是一个只有副作用的导入:它会在程序启动时读取 .env,后面的代码就可以直接从 process.env 取值。

baseURL 让 ChatOpenAI 可以连接兼容 OpenAI 协议的服务。temperature: 0 用来降低回答的随机性,适合观察中间件是否按预期执行,但它不等于数学意义上的绝对确定。

二、先建立 Agent 的执行模型

理解中间件之前,先不要把 Agent 想成一次模型请求。带工具的 Agent 更接近下面这个循环:

text 复制代码
beforeAgent(一次)
    ↓
beforeModel
    ↓
wrapModelCall → 模型调用
    ↓
afterModel
    ↓
模型是否请求工具?
    ├─ 否 → afterAgent(一次)→ 结束
    └─ 是 → wrapToolCall → 执行工具
                         ↓
                     回到 beforeModel

这张流程图能解释两个经常混淆的问题。

第一,beforeAgent 和 afterAgent 面向一次完整的 Agent 调用;beforeModel 和 afterModel 面向一次模型调用。只要 Agent 调用工具后再次询问模型,模型级钩子就会再次执行。

第二,wrapModelCall 与 wrapToolCall 是包装器。它们拿到一个 handler,只有调用 handler(request),后续中间件或真正的模型、工具才会继续执行。因此它们天然适合做参数改写、日志、重试和结果包装。

三、用生命周期钩子统计模型调用次数

先实现一个日志中间件。它负责打印 Agent 的开始和结束、每次模型调用前后的信息,并在状态中维护模型调用次数。

js 复制代码
import { z } from "zod";
import { createMiddleware } from "langchain";

const loggingMiddleware = createMiddleware({
  name: "LoggingMiddleware",

  stateSchema: z.object({
    modelCallCount: z.number().default(0),
  }),

  beforeAgent: (state) => {
    console.log(
      `\n[Logging] Agent 开始,消息数:${state.messages.length}\n`
    );
  },

  beforeModel: (state) => {
    console.log(
      `[Logging] 即将调用模型,当前消息数:${state.messages.length},` +
      `已调用 ${state.modelCallCount} 次模型\n`
    );
  },

  afterModel: (state) => {
    const last = state.messages.at(-1);
    const preview =
      typeof last?.content === "string"
        ? last.content.slice(0, 200)
        : JSON.stringify(last?.content).slice(0, 200);

    console.log(`[Logging] 模型返回:${preview}...\n`);

    return {
      modelCallCount: state.modelCallCount + 1,
    };
  },

  afterAgent: (state) => {
    console.log(
      `[Logging] Agent 结束,累计模型调用:${state.modelCallCount} 次\n`
    );
  },
});

1. stateSchema 不只是参数校验

这里的 Zod Schema 定义了中间件拥有的状态字段:

js 复制代码
stateSchema: z.object({
  modelCallCount: z.number().default(0),
})

default(0) 很重要,因为调用 Agent 时不需要手动传入 modelCallCount。框架会初始化这个字段,生命周期钩子随后可以通过 state.modelCallCount 读取它。

钩子不必直接修改 state,返回一个局部状态更新即可:

js 复制代码
return {
  modelCallCount: state.modelCallCount + 1,
};

这样做更符合状态图的工作方式:节点产出"这一步要更新什么",再由图把更新合并进总状态。

2. 为什么消息内容需要分类型处理

下面这段代码没有直接对 content 调用 slice:

js 复制代码
const preview =
  typeof last?.content === "string"
    ? last.content.slice(0, 200)
    : JSON.stringify(last?.content).slice(0, 200);

原因是消息内容不一定永远是纯字符串,它也可能是结构化内容块。先判断类型,再生成日志摘要,可以避免日志代码反过来破坏 Agent 主流程。

3. "一次 Agent 调用"不等于"一次模型调用"

如果模型直接回答,modelCallCount 通常增加一次。如果模型先发出工具调用,工具返回结果后模型还要组织最终答案,那么模型通常会被调用两次:

text 复制代码
用户消息 → 模型决定调用工具 → 工具结果 → 模型生成最终回答
             第 1 次                    第 2 次

这也是把统计逻辑放在 afterModel 而不是 afterAgent 中的原因。

四、用 wrapModelCall 动态补充系统消息

有些约束并不适合硬编码进 Agent 的基础系统提示词。例如,不同请求可能需要不同的回答风格、租户上下文或临时规则。此时可以在模型真正收到请求前改写系统消息。

js 复制代码
const addContextMiddleware = createMiddleware({
  name: "AddContextMiddleware",

  wrapModelCall: async (request, handler) => {
    console.log("[Add Context] 注入额外的 System 上下文");

    return handler({
      ...request,
      systemMessage: request.systemMessage.concat(
        "\n\n请用一句话间接回答"
      ),
    });
  },
});

这里要抓住三个细节。

1. 展开原请求,只覆盖需要修改的字段

request 中不只有系统消息,还包含模型、普通消息、工具和运行时状态等信息。使用 ...request 保留原请求,再覆盖 systemMessage,可以避免遗漏其他字段。

2. 当前版本中优先操作 systemMessage

request.systemMessage 是一条 SystemMessage,concat 会保留原系统消息,再追加文本。这样基础指令"你是一个助手"仍然存在,只是在本次模型调用时多了一条回答约束。

3. 一定要调用并返回 handler

js 复制代码
return handler(modifiedRequest);

这一步才会把改写后的请求交给后续链路。忘记调用 handler,模型不会正常执行;调用了却不返回,外层也拿不到模型响应。

五、用 jumpTo 在模型调用前短路

如果某类请求根本不应该发给模型,最有效的处理方式不是在模型返回后再过滤,而是在 beforeModel 阶段结束流程。

js 复制代码
import { AIMessage } from "langchain";

const blockContentMiddleware = createMiddleware({
  name: "BlockedContentMiddleware",

  beforeModel: {
    canJumpTo: ["end"],

    hook: (state) => {
      const last = state.messages.at(-1);
      const text =
        typeof last?.content === "string"
          ? last.content
          : String(last?.content ?? "");

      if (text.includes("BLOCKED")) {
        console.log("[Blocked] 检测到 BLOCKED,短路结束");

        return {
          messages: [
            new AIMessage("该请求已被 middleware 拦截,无法处理"),
          ],
          jumpTo: "end",
        };
      }
    },
  },
});

这里同时完成了两件事:

  • 向消息状态追加一条 AIMessage,让调用方仍然能从最后一条消息中取得明确回复;
  • 返回 jumpTo: "end",告诉执行图不要进入模型节点。

canJumpTo: ["end"] 则是在声明这个钩子允许跳到哪里。它和 jumpTo 是配套的:前者声明可用目标,后者在运行时真正选择目标。

需要特别注意:这个短路发生在 beforeModel。在当前执行图中,它会直接跳到结束节点,因此模型不会调用,afterModel 不会执行,afterAgent 也不会执行。不要把必须执行的资源清理仅仅寄托在这个 afterAgent 钩子上。

将三个中间件组装起来:

js 复制代码
import {
  createAgent,
  HumanMessage,
} from "langchain";

const agent = createAgent({
  model,
  tools: [],
  systemPrompt: "你是一个助手。",
  middleware: [
    loggingMiddleware,
    addContextMiddleware,
    blockContentMiddleware,
  ],
});

const { messages, modelCallCount } = await agent.invoke({
  messages: [new HumanMessage("这句话包含 BLOCKED 关键词")],
});

console.log("回复:", messages.at(-1)?.content);
console.log("modelCallCount:", modelCallCount);

这次调用的核心输出是:

text 复制代码
[Logging] Agent 开始,消息数:1
[Logging] 即将调用模型,当前消息数:1,已调用 0 次模型
[Blocked] 检测到 BLOCKED,短路结束
回复:该请求已被 middleware 拦截,无法处理
modelCallCount:0

为什么 AddContextMiddleware 没有打印日志?因为它使用的是 wrapModelCall,只有执行图真的走到模型节点时才会运行。请求已经在 beforeModel 被截断,自然不会进入模型包装器。

六、让中间件自己提供工具

中间件除了观察和拦截,还能携带工具。下面定义一个返回当前 UTC 时间的工具:

js 复制代码
import { z } from "zod";
import { tool } from "langchain";

const getCurrentTime = tool(
  () => new Date().toISOString(),
  {
    name: "get_current_time",
    description: "返回当前 UTC 时间的 ISO 8601 字符串",
    schema: z.object({}),
  }
);

即使工具没有参数,仍然使用 z.object({}) 明确声明一个空对象 Schema。工具名和描述会提供给模型,模型据此判断什么时候调用它。

接下来创建中间件,把工具注册进去,同时统计它被调用的次数:

js 复制代码
import { Command } from "@langchain/langgraph";
import {
  createMiddleware,
  ToolMessage,
} from "langchain";

const extendedToolMiddleware = createMiddleware({
  name: "ExtendedToolsMiddleware",

  stateSchema: z.object({
    toolInvocationCount: z.number().default(0),
  }),

  tools: [getCurrentTime],

  wrapToolCall: async (request, handler) => {
    const toolName = request.tool?.name ?? request.toolCall.name;

    console.log(
      `[Tools] 即将执行:${toolName}`,
      "args",
      request.toolCall.args ?? {}
    );

    const result = await handler(request);

    if (!ToolMessage.isInstance(result)) {
      return result;
    }

    const wrapped = new ToolMessage({
      content: `${result.content}\n[wrapToolCall] 已由 ExtendedToolsMiddleware 包裹`,
      tool_call_id: result.tool_call_id,
      name: result.name,
    });

    return new Command({
      update: {
        toolInvocationCount:
          request.state.toolInvocationCount + 1,
        messages: [wrapped],
      },
    });
  },

  afterAgent: (state) => {
    console.log(
      `[Tool] Agent 结束,中间件统计工具调用:` +
      `${state.toolInvocationCount} 次`
    );
  },
});

1. 顶层 tools 为空,模型为什么仍能看到工具

创建 Agent 时可以这样写:

js 复制代码
const agent = createAgent({
  model,
  tools: [],
  systemPrompt: "你是一个助手",
  middleware: [extendedToolMiddleware],
});

顶层 tools: [] 只表示 Agent 自身没有直接注册工具。extendedToolMiddleware 的 tools: [getCurrentTime] 会把工具加入 Agent 可用工具集合,所以模型仍能选择 get_current_time。

这种组织方式的价值在于内聚:工具、工具调用日志、结果包装和调用次数统计都放在同一个中间件中,Agent 顶层只负责组合能力。

2. handler(request) 才是真正执行工具

wrapToolCall 在工具外面包了一层:

js 复制代码
const result = await handler(request);

调用前可以检查名称和参数,调用后可以检查或改写结果。这里的 request 主要用到了:

  • request.toolCall.name:模型发出的工具调用名称;
  • request.toolCall.args:模型生成的工具参数;
  • request.tool:已经解析出的工具实例,动态场景下可能不存在,所以名称读取提供了回退;
  • request.state:当前 Agent 状态,包括中间件声明的计数器。

3. 为什么先判断 ToolMessage

工具包装链允许返回 ToolMessage,也允许返回用于高级流程控制的 Command。只有确认结果是 ToolMessage,才能安全读取 content、tool_call_id 和 name 并重新包装:

js 复制代码
if (!ToolMessage.isInstance(result)) return result;

如果已经是 Command,这里保持原样返回,避免误把控制指令当成普通工具结果。

4. 为什么返回 Command,而不是只返回新的 ToolMessage

这个中间件想一次完成两项更新:

  1. 把包装后的工具结果加入消息历史;
  2. 将 toolInvocationCount 加一。

Command 正适合表达"更新状态,并继续由状态图处理后续流程":

js 复制代码
return new Command({
  update: {
    toolInvocationCount: request.state.toolInvocationCount + 1,
    messages: [wrapped],
  },
});

messages 是 Agent 的内置消息通道,这里的数组会通过对应的累加逻辑并入历史,而不是把整段会话简单替换掉。

调用示例:

js 复制代码
const { messages, toolInvocationCount } = await agent.invoke({
  messages: [new HumanMessage("给我当前时间")],
});

console.log(messages);
console.log("toolInvocationCount:", toolInvocationCount);

一次典型流程是:模型先请求 get_current_time,工具返回 ISO 时间,中间件包装 ToolMessage 并把计数加一,模型再根据工具结果生成自然语言回答。因此,工具调用次数通常是 1,而模型调用次数通常是 2,两者统计的不是同一件事。

七、用 DeepAgents 给 Agent 一个受控工作区

前面的工具由我们逐个定义。文件操作包含列目录、读取、写入、编辑、搜索等一组能力,如果全部手写会比较繁琐。createFilesystemMiddleware 可以把这些能力作为中间件整体接入 Agent。

先构造本地工作区路径:

js 复制代码
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const workspaceDir = path.join(
  path.dirname(fileURLToPath(import.meta.url)),
  "workspace"
);

fs.mkdirSync(workspaceDir, { recursive: true });
fs.writeFileSync(
  path.join(workspaceDir, "secret.txt"),
  "机密:不得读取",
  "utf-8"
);

ESM 中没有 CommonJS 的 __dirname,import.meta.url 又是 file:// URL,因此先用 fileURLToPath 转成本地路径,再取当前目录。

这里使用 Node.js 自己的 fs 初始化测试数据。它发生在 Agent 执行之前,不经过文件系统中间件,因此不受后面权限规则约束。权限限制的是 Agent 通过中间件工具发起的操作,不是宿主程序自身的所有文件调用。

1. 设计最小写权限

目标是:

  • 禁止 Agent 读取 /secret.txt;
  • 只允许写入和编辑 /todo.md;
  • 其他文件一律禁止写入;
  • 其他读取操作保持允许,以便 Agent 能列目录和检查任务文件。

权限规则如下:

js 复制代码
const permissions = [
  {
    operations: ["read"],
    paths: ["/secret.txt"],
    mode: "deny",
  },
  {
    operations: ["write"],
    paths: ["/todo.md"],
    mode: "allow",
  },
  {
    operations: ["write"],
    paths: ["/**"],
    mode: "deny",
  },
];

权限判断有两个非常关键的规则:

  1. 按声明顺序检查,第一条匹配的规则生效。
  2. 没有任何规则匹配时,默认允许。

因此,/todo.md 的允许规则必须放在全局写入拒绝规则之前。否则 /todo.md 会先命中 /**,后面的精确允许规则没有机会生效。

把常见操作展开后会更直观:

操作 路径 命中的规则 结果
读 /secret.txt 第一条 拒绝
读 /todo.md 无 默认允许
写 /todo.md 第二条 允许
写 /secret.txt 第三条 拒绝
写 /notes.md 第三条 拒绝

当 Agent 执行 ls / 时,列目录本身可以进行,但结果还会按照读取权限过滤,因此被拒绝读取的 /secret.txt 不应暴露在列表结果中。

2. 权限必须配置在中间件这一层

这是最容易写错、也最隐蔽的地方。FilesystemBackend 负责"文件操作具体落到哪里",createFilesystemMiddleware 负责"给 Agent 暴露什么工具,以及如何做权限判断"。所以正确层级是:

js 复制代码
createFilesystemMiddleware({
  backend: new FilesystemBackend({
    rootDir: workspaceDir,
    virtualMode: true,
  }),
  permissions,
});

不要把 permissions 塞进 new FilesystemBackend(...)。JavaScript 对多余对象字段不会自动报错,但 Backend 并不读取这个字段,最终表现就是规则看起来已经配置,实际上没有生效。

3. virtualMode 把真实目录映射成虚拟根目录

js 复制代码
new FilesystemBackend({
  rootDir: workspaceDir,
  virtualMode: true,
})

开启 virtualMode 后,模型看到的是一套虚拟绝对路径:

text 复制代码
/todo.md
/secret.txt

它们在底层会映射到:

text 复制代码
<workspaceDir>/todo.md
<workspaceDir>/secret.txt

因此,系统提示词中的"工作区根路径为 /"指的是虚拟根目录,不是操作系统真正的 /。这种映射既让模型更容易使用绝对路径,也把它的文件操作限制在 rootDir 内。

4. 完整组装文件系统 Agent

js 复制代码
import "dotenv/config";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ChatOpenAI } from "@langchain/openai";
import {
  createFilesystemMiddleware,
  FilesystemBackend,
} from "deepagents";
import {
  createAgent,
  HumanMessage,
} from "langchain";

const workspaceDir = path.join(
  path.dirname(fileURLToPath(import.meta.url)),
  "workspace"
);

fs.mkdirSync(workspaceDir, { recursive: true });
fs.writeFileSync(
  path.join(workspaceDir, "secret.txt"),
  "机密:不得读取",
  "utf-8"
);

const permissions = [
  {
    operations: ["read"],
    paths: ["/secret.txt"],
    mode: "deny",
  },
  {
    operations: ["write"],
    paths: ["/todo.md"],
    mode: "allow",
  },
  {
    operations: ["write"],
    paths: ["/**"],
    mode: "deny",
  },
];

const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
  temperature: 0,
});

const agent = createAgent({
  model,
  tools: [],
  systemPrompt: `
    工作区根路径为 /。用 ls、read_file、write_file、edit_file 操作文件,
    路径以 / 开头。使用中文回答。
  `,
  middleware: [
    createFilesystemMiddleware({
      backend: new FilesystemBackend({
        rootDir: workspaceDir,
        virtualMode: true,
      }),
      permissions,
    }),
  ],
});

async function run(label, prompt) {
  console.log(`\n===== ${label} =====`);

  const { messages } = await agent.invoke(
    {
      messages: [new HumanMessage(prompt)],
    },
    {
      recursionLimit: 20,
    }
  );

  console.log(messages);
}

await run(
  "允许的操作",
  "write_file 创建 /todo.md(三条代办)," +
    "edit_file 把其中一条标为完成,ls /,一句话总结"
);

虽然 Agent 顶层仍是 tools: [],文件系统中间件会提供 ls、read_file、write_file、edit_file 等工具。FilesystemBackend 不具备执行 shell 命令的能力,因此这里的重点是结构化文件工具,而不是任意命令执行。

recursionLimit: 20 限制的是状态图允许推进的最大步数。文件任务通常包含"模型决策 → 写文件 → 再次决策 → 编辑文件 → 再次决策 → 列目录 → 总结"等多个节点,设置上限可以避免异常情况下无限循环。

如果 /todo.md 最终内容类似:

md 复制代码
- [x] 买菜
- [ ] 洗衣服
- [ ] 写周报

说明 write_file 和 edit_file 都成功工作。具体哪一项被标记为完成由模型决定,示例只要求完成其中一项。

八、三种消息对象分别解决什么问题

几个示例里出现了三类消息,它们的角色不同:

类型 发送者角色 本文中的用途
HumanMessage 用户 将自然语言任务放入 Agent 输入
AIMessage 模型或 Agent 短路时构造一条可直接返回给用户的回复
ToolMessage 工具 将工具结果与对应的 tool_call_id 关联起来

其中 tool_call_id 不能随意丢失。模型可能一次发出多个工具调用,框架需要靠它判断每个结果对应哪个调用。重新包装 ToolMessage 时至少要保留 tool_call_id,同时保留工具名。

九、把几个关键边界再梳理一遍

1. Hook 和 Wrapper 不要混用

  • beforeAgent、beforeModel、afterModel、afterAgent 更适合观察状态、返回局部状态更新或控制跳转;
  • wrapModelCall、wrapToolCall 更适合包裹真正的调用过程,改请求、执行 handler、再处理结果。

如果只是给模型补一段系统消息,wrapModelCall 比在全局到处拼 Prompt 更集中。如果要在模型调用前直接拒绝请求,beforeModel + jumpTo 更直接。

2. 状态计数要放在真正对应的事件上

  • 模型调用计数放在 afterModel;
  • 工具调用计数放在 wrapToolCall 成功执行 handler 之后;
  • 不要用 afterAgent 同时猜测模型和工具分别执行了多少次。

3. 短路会改变后续钩子的执行机会

jumpTo: "end" 不只是返回一个标记,它会改变执行图的路径。在 beforeModel 中直接结束后,模型包装器、模型节点、afterModel 以及这里的 afterAgent 都不会继续执行。因此,日志中看不到这些阶段不是异常,而是短路本身的结果。

4. 虚拟路径隔离与权限规则是两层保护

virtualMode: true 解决"Agent 能活动到哪里",permissions 解决"在允许活动的范围内,哪些路径能读或写"。两者互相补充,但不能互相替代:

  • 只有虚拟目录,没有权限规则:Agent 仍可以修改工作区内的所有文件;
  • 只有路径规则,没有虚拟目录:路径映射和根目录边界不够直观;
  • 两者同时使用:先限制根目录,再对目录内文件做细粒度控制。

5. 规则顺序和默认放行决定最终权限

下面两组规则看起来相似,结果却完全不同:

js 复制代码
// 正确:精确允许在前,兜底拒绝在后
[
  { operations: ["write"], paths: ["/todo.md"], mode: "allow" },
  { operations: ["write"], paths: ["/**"], mode: "deny" },
]
js 复制代码
// 错误:/todo.md 会先命中全局拒绝
[
  { operations: ["write"], paths: ["/**"], mode: "deny" },
  { operations: ["write"], paths: ["/todo.md"], mode: "allow" },
]

另外,只配置"禁止读取 secret"并不代表其他读取也被禁止,因为未匹配规则时默认允许。设计白名单时,要显式加入相应的兜底拒绝规则。

十、总结

中间件真正有价值的地方,不是少写几个 console.log,而是把 Agent 的横切能力变成可组合的执行单元:

  • stateSchema 为中间件增加有默认值、可参与状态图更新的字段;
  • 生命周期钩子让我们在 Agent 或模型调用的关键节点观察、更新和截断流程;
  • wrapModelCall 可以在模型调用前动态修改系统消息;
  • beforeModel 配合 canJumpTo、jumpTo 和 AIMessage,可以在零模型调用的情况下返回拦截结果;
  • 中间件的 tools 可以独立向 Agent 注入工具;
  • wrapToolCall 配合 ToolMessage 和 Command,可以同时包装结果、维护消息关联并更新自定义状态;
  • FilesystemBackend 负责真实存储与虚拟路径映射,createFilesystemMiddleware 负责工具暴露和权限执行;
  • 文件权限采用"按顺序首条匹配、未匹配默认允许",规则顺序必须经过仔细设计。

把这些边界理顺之后,日志、审计、内容拦截、动态上下文、工具扩展和文件权限就不再需要散落在 Agent 主流程中。Agent 负责推理和选择动作,中间件负责约束、观测和扩展,两者的职责会清晰很多。

相关推荐
志尊宝2 小时前
Vue3 零基础每日笔记(082):文件上传实战——el-upload 校验、自定义上传、头像预览
前端·javascript·vue.js·笔记·#javascript·#vue.js
10年前端老司机6 小时前
我用 Vue3 + LangGraph 做了个心理咨询 AI,踩了 9 个坑后总结出来的实战经验
前端·langchain·agent
bmxy小明同学8 小时前
2026-10-11-幻觉治理
服务器·前端·javascript
m0_587383008 小时前
无人自助拼豆系统落地开发实战指南 核心逻辑实现与部署经验分享
前端·javascript·vue.js·人工智能·经验分享·需求分析
zhaiyy9 小时前
HTML Popover API 实践
前端·javascript·html
一木 之林9 小时前
LangChain 第3章 消息与提示词模板:四类消息、历史管理与 ChatPromptTemplate
人工智能·计算机视觉·langchain
OpsEye9 小时前
企业 AI 管理新需求:不仅要接入模型,还要看懂每一次调用
javascript·ai编程
牧艺9 小时前
SPA 也能像原生 App:View Transitions API 从原理到可上线实战
前端·javascript·css