MCP策略代理实战:给LLM工具调用加一道模型碰不到的权限闸门

📅 发布时间:2026/10/17 7:02:20
MCP策略代理实战:给LLM工具调用加一道模型碰不到的权限闸门
最近在 Hacker News 上刷到一个项目标题原文是A Proxy between LLMs and MCP servers with policy the model cannot reach。翻译过来就是在 LLM 和 MCP 服务器之间加一个策略代理并且策略是模型碰不到的。这个项目名看起来很绕但它解决的问题非常具体当大模型可以调用工具时如何保证工具调用不会失控。本文会把这个项目背后的架构思想拆开讲清楚并用 Python MCP SDK 从零实现一个最小可运行的策略代理覆盖工具白名单、参数校验、审计日志、调用转发这几个核心环节。如果你是做 AI Agent、MCP 开发、或者正在考虑“如何给 LLM 工具调用加权限控制”这篇文章可以直接作为一份落地参考。1. 背景MCP 与“不受控”的工具调用1.1 MCP 是什么MCP 全称 Model Context Protocol也就是模型上下文协议。它是由 Anthropic 发起的一套开放协议目标是让 LLM 应用能够统一连接外部数据源和工具。在 MCP 之前的做法通常是“各家自己写插件协议”模型要读数据库写一套要调文件系统又写一套非常碎片化。MCP 出现之后客户端和服务器之间有了标准接口MCP Client一般是 LLM 应用、IDE 插件或 AI Agent 框架。MCP Server对外暴露能力包括 Tools工具、Resources资源、Prompts提示词模板。Tools模型可以主动调用的函数例如查询数据库、读取文件、发送 HTTP 请求。模型通过客户端调用工具的过程可以简单理解为模型输出“我想调用 file_read参数是 path/tmp/a.txt”客户端收到指令后通过 MCP 协议把请求转给对应的 MCP Server工具执行完毕后结果再原路返回给模型。1.2 为什么模型调用工具会失控工具调用扩展了 LLM 的能力但也把风险边界从“模型说话”扩展到了“模型能动手”。举几个典型场景模型拿到了一个工具列表里面包含 file_delete、shell_run、db_execute 这类敏感工具。提示注入攻击让模型产生了错误判断主动去调用删除或写入工具。模型在推理时对参数缺少约束可能把路径参数传成了绝对路径删除了不该删的文件。即便你在系统提示词里写了“绝对不要删除文件”LLM 本质上还是概率模型这条规则并不是硬约束。在一个真实业务系统里依赖“模型自觉”是不可靠的。我们需要的一种思路是把策略下沉到执行链路而不是只写在模型上下文里。1.3 策略代理的定位策略代理可以理解为一个“强制检查站”位置在 MCP 客户端与真正执行任务的 MCP Server 之间。从客户端和模型的角度看代理就是一台 MCP Server。从后端的实际服务角度看代理又像是一个 MCP Client。它负责转发工具列表、拦截工具调用、执行策略检查、记录审计日志然后决定请求是否放行。这个设计最直接的好处是模型无法通过在提示词里“自我暗示”绕过规则。因为规则根本不在模型能影响的范围内规则在代理进程里在策略文件中在代码逻辑中。2. 策略代理的核心思路2.1 总体架构整个系统的连接关系如下------------ tools/call ------------------ tools/call ------------------ | LLM 客户端 | ------------- | 策略代理 Proxy | ------------- | 后端 MCP Server | ------------ ------------------ ------------------ | | 读取 v 策略文件/规则引擎 | v 审计日志当模型端向代理请求工具列表时代理会先从后端 MCP Server 拉取真实工具再经过策略过滤只把允许暴露的工具返回给模型端。当模型调用工具时代理先执行策略判断允许则转发给后端拒绝则直接返回错误整个过程对模型端透明可见但无法影响判断结果。2.2 “模型碰不到策略”是什么意思标题里的 policy the model cannot reach核心是策略与模型之间存在隔离边界。常规做法是“把你的安全要求写到系统提示词里”这种做法本质上是依赖模型随机地遵守概率最高的规则。一旦模型被骗、被注入、或者上下文被污染规则就可能被忽略。策略代理的做法是把规则放到另一个执行环境中策略文件存储在一个模型无法读写的路径。模型能感知到的只有“代理暴露给它的工具列表”。即使模型猜到了某个隐藏工具名代理也会在调用转发前拦截。即使模型被提示注入控制实际工具调用也需要通过代理策略检查。所以这里说的“模型碰不到”不是指模型不知道规则而是指模型没有能力在执行路径上修改或绕过规则。2.3 与普通权限系统的区别传统权限系统往往围绕“用户”建模例如“某个用户是否有权删除文件”。而策略代理主要围绕“模型调用行为”建模更关注的是模型可以看见哪些工具。模型可以调用哪些工具。调用参数是否符合约束。调用是否被审计记录。它既可以配合用户权限体系一起用也可以独立承担“工具调用降险”这一层职责。如果你已经用了网关、API 权限、RBAC 系统策略代理可以继续叠加在工具调用层形成纵深防御。3. 环境准备与工程结构3.1 运行环境推荐环境如下示例已在 Python 3.10 环境验证操作系统Linux / macOSWindows 也可以运行但要注意路径分隔符差异。Python3.10 及以上。MCP SDK官方 Python SDK包名是 mcp。依赖PyYAML用于读取策略配置文件。MCP 的 Python SDK 更新比较快不同小版本的 API 可能略有差异。本文示例基于常见的 mcp 包接口编写如果版本变化较大请以你实际安装版本的官方文档为准。3.2 创建项目目录我们创建一个示例工程目录结构如下llm-mcp-policy-proxy/ ├── config/ │ └── proxy.yaml ├── logs/ │ └── audit.jsonl ├── mcp_backend/ │ └── backend_server.py ├── proxy/ │ ├── __init__.py │ ├── audit.py │ ├── policy_engine.py │ └── proxy_server.py ├── tests/ │ └── demo_client.py ├── workspace/ │ └── notes.md └── requirements.txt其中mcp_backend模拟一个真实的 MCP Server提供文件读写等工具。proxy策略代理核心代码。config/proxy.yaml策略配置。tests/demo_client.py模拟 LLM 客户端进行端到端验证。3.3 安装依赖创建 requirements.txtmcp pyyaml安装命令pip install -r requirements.txt如果之前没有安装过 mcp建议安装后执行一下版本检查pip show mcp确认版本后再开始后续开发。4. 核心原理拆解4.1 代理的双通道身份策略代理中最关键的一点是它同时具备两重身份。在模型端看来代理是一个标准 MCP Server。MCP 客户端连接代理时需要使用 server 初始化握手向客户端暴露工具列表接收 tools/call 请求。在后端 MCP Server 看来代理是一个标准 MCP Client。代理启动时需要连接后端服务器调用 initialize 完成握手拉取后端工具列表然后以 ClientSession 的身份调用后端工具。你可以把代理理解成一个“MCP 网关”既要会讲服务端的语言也要会讲客户端的语言。4.2 工具列表过滤MCP 的 list_tools 过程非常关键。后端服务器返回所有真实工具包括 file_read、file_write、file_delete、shell_run。代理不会把这些工具原样暴露给模型端而是先经过策略引擎过滤mode: allowlist allow: - name: file_read - name: file_write deny: - name: file_delete - name: shell_run如果配置为 allowlist 模式模型端能看到的工具就只有 file_read 和 file_write。file_delete、shell_run 对模型来说是完全隐藏在列表之外的。这一步非常重要。因为模型在决策时会依赖工具描述来生成调用。你不在列表里给出“删除工具”模型就不会主动生成删除调用。即使模型被诱导尝试猜测调用 file_delete代理层的策略引擎也会再次拒绝。4.3 调用拦截当模型端发起 tools/call 请求时代理并不会傻傻地直接转发给后端而是先执行一个完整判断流程从请求中解析工具名和参数。查找 deny 规则如果该工具被禁止直接拒绝。如果模式是 allowlist检查工具是否在允许列表中。检查参数约束例如路径是否允许范围内。记录审计日志。通过策略检查后才调用后端 ClientSession.call_tool。将后端返回结果转发给模型端。这里建议把“策略检查”和“后端调用”拆成两个独立模块。策略模块只负责决策后端模块只负责转发。这样以后要扩展多个后端时只需要增加新的连接配置策略引擎不用改。5. 完整实战构建一个策略代理5.1 创建核心策略配置文件先创建 config/proxy.yamlbackend: command: python args: - mcp_backend/backend_server.py policy: mode: allowlist allow: - name: file_read arguments: path: allowed_patterns: - workspace/** - docs/* - name: file_write arguments: path: allowed_patterns: - workspace/** deny: - name: file_delete - name: shell_run配置说明backend代理启动时要通过 stdio 启动后端的 MCP Server。policy.modeallowlist 表示白名单模式未明确允许的工具一律禁止。allow允许模型调用的工具及参数约束。deny即使工具在其他规则中出现过只要命中 deny 也拒绝。allowed_patterns路径匹配模式** 表示递归子目录* 表示当前目录一层。5.2 编写后端 MCP Server创建 mcp_backend/backend_server.py# mcp_backend/backend_server.py import os import subprocess from mcp.server.fastmcp import FastMCP mcp FastMCP(demo-backend-server) mcp.tool() def file_read(path: str) - str: 读取文本文件内容。 with open(path, r, encodingutf-8) as f: return f.read() mcp.tool() def file_write(path: str, content: str) - str: 将内容写入指定文本文件。 with open(path, w, encodingutf-8) as f: f.write(content) return f文件写入成功{path} mcp.tool() def file_delete(path: str) - str: 永久删除指定文件高风险操作。 os.remove(path) return f文件删除成功{path} mcp.tool() def shell_run(command: str) - str: 在环境中执行 shell 命令高风险操作。 result subprocess.run( command, shellTrue, textTrue, capture_outputTrue, timeout10, ) return (result.stdout result.stderr).strip() or (无输出) if __name__ __main__: mcp.run()这是一个标准 FastMCP 服务启动后通过 stdio 与父进程通信。它本身不知道上游是谁也无法判断调用方是否可信。真正做判断的职责全部交给策略代理。5.3 编写策略引擎创建 proxy/policy_engine.py# proxy/policy_engine.py import fnmatch import yaml class PolicyDecision: def __init__(self, allowed: bool, reason: str ): self.allowed allowed self.reason reason class PolicyEngine: def __init__(self, config: dict): self.mode config.get(mode, allowlist) self.allow_rules config.get(allow, []) self.deny_rules config.get(deny, []) classmethod def from_file(cls, path: str) - PolicyEngine: with open(path, r, encodingutf-8) as f: data yaml.safe_load(f) return cls(data[policy]) def filter_tools(self, backend_tools): denied_names {rule[name] for rule in self.deny_rules} if self.mode denylist: return [t for t in backend_tools if t.name not in denied_names] allowed_names {rule[name] for rule in self.allow_rules} return [ t for t in backend_tools if t.name in allowed_names and t.name not in denied_names ] def evaluate(self, tool_name: str, arguments: dict) - PolicyDecision: # 1. 首先检查拒绝规则 for rule in self.deny_rules: if rule[name] tool_name and self._match_rule(rule, arguments): return PolicyDecision(False, f工具 {tool_name} 命中拒绝策略) # 2. denylist 模式下只要没被拒绝就放行 if self.mode denylist: return PolicyDecision(True, ) # 3. allowlist 模式下必须显式命中允许规则 for rule in self.allow_rules: if rule[name] ! tool_name: continue if self._match_rule(rule, arguments): return PolicyDecision(True, ) return PolicyDecision(False, f工具 {tool_name} 的参数不符合策略约束) return PolicyDecision(False, f工具 {tool_name} 不在允许列表中) def _match_rule(self, rule: dict, arguments: dict) - bool: arg_rules rule.get(arguments) or {} if not arg_rules: return True for arg_name, constraint in arg_rules.items(): if arg_name not in arguments: return False patterns constraint.get(allowed_patterns, []) if not self._match_path_like(str(arguments[arg_name]), patterns): return False return True staticmethod def _normalize_path(value: str) - str: return value.replace(\\, /) def _match_path_like(self, value: str, patterns: list) - bool: value self._normalize_path(value).lstrip(./) for pattern in patterns: pattern self._normalize_path(pattern) # workspace/** 匹配 workspace 下所有子路径 if pattern.endswith(/**): prefix pattern[:-3].rstrip(/) if value prefix or value.startswith(prefix /): return True # docs/* 只匹配 docs 下一级 elif pattern.endswith(/*): prefix pattern[:-2].rstrip(/) if value.startswith(prefix /) and / not in value[len(prefix) 1:]: return True elif fnmatch.fnmatch(value, pattern): return True elif value.startswith(pattern.rstrip(/)): return True return False这个策略引擎有几个设计点值得注意先判断 deny再匹配 allow这样即使 allow 规则写宽了deny 仍然有最高优先级。路径匹配采用了独立方法后续要改成正则、前缀匹配或容器路径映射都很方便。路径统一把反斜杠转成正斜杠降低 Windows 与 Linux 之间的差异影响。5.4 编写审计模块创建 proxy/audit.py# proxy/audit.py import json import os from datetime import datetime, timezone class AuditLogger: def __init__(self, path: str): self.path path os.makedirs(os.path.dirname(self.path) or ., exist_okTrue) def log(self, **entry) - None: entry[ts] datetime.now(timezone.utc).isoformat() with open(self.path, a, encodingutf-8) as f: f.write(json.dumps(entry, ensure_asciiFalse) \n)审计日志采用 JSON Lines 格式每条记录独立一行。这样可以方便对接日志采集系统例如直接由 Filebeat、Logstash、Promtail 采集。5.5 编写策略代理服务器创建 proxy/proxy_server.py# proxy/proxy_server.py import asyncio from contextlib import AsyncExitStack import mcp.server.stdio import yaml from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from mcp.server import Server from mcp.server.models import InitializationOptions from mcp.types import CallToolResult, TextContent, Tool from proxy.audit import AuditLogger from proxy.policy_engine import PolicyEngine server Server(llm-policy-proxy) policy: PolicyEngine audit: AuditLogger backend_session: ClientSession | None None async def connect_backend(config: dict, exit_stack: AsyncExitStack) - None: global backend_session backend config.get(backend, {}) params StdioServerParameters( commandbackend.get(command, python), argsbackend.get(args, [mcp_backend/backend_server.py]), ) stdio_transport await exit_stack.enter_async_context(stdio_client(params)) read_stream, write_stream stdio_transport backend_session await exit_stack.enter_async_context( ClientSession(read_stream, write_stream) ) await backend_session.initialize() server.list_tools() async def handle_list_tools() - list[Tool]: if backend_session is None: return [] backend_tools (await backend_session.list_tools()).tools return policy.filter_tools(backend_tools) server.call_tool() async def handle_call_tool(name: str, arguments: dict) - CallToolResult: decision policy.evaluate(name, arguments or {}) audit.log( eventTOOL_CALL, toolname, argsarguments or {}, alloweddecision.allowed, reasondecision.reason, ) if not decision.allowed: return CallToolResult( content[ TextContent( typetext, textf[POLICY BLOCKED] 工具 {name} 被策略拦截原因{decision.reason}, ) ], isErrorTrue, ) if backend_session is None: return CallToolResult( content[TextContent(typetext, text[PROXY] 后端通道未初始化)], isErrorTrue, ) result await backend_session.call_tool(name, arguments or {}) audit.log( eventTOOL_CALL_BACKEND, toolname, argsarguments or {}, backend_is_errorresult.isError, ) return CallToolResult(contentresult.content, isErrorresult.isError) async def main() - None: global policy, audit with open(config/proxy.yaml, r, encodingutf-8) as f: config yaml.safe_load(f) policy PolicyEngine.from_file(config/proxy.yaml) audit AuditLogger(logs/audit.jsonl) async with AsyncExitStack() as exit_stack: await connect_backend(config, exit_stack) async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, InitializationOptions( server_namellm-policy-proxy, server_version0.1.0, ), ) if __name__ __main__: asyncio.run(main())这里有几个实现细节需要说明。第一代理持有两个会话一个是面向模型端调用方的 Server另一个是面向后端执行方的 ClientSession。第二list_tools 必须过滤后返回而不是原样透传。这直接决定了模型“能看见哪些工具”。第三call_tool 里先做策略决策再调用后端并记录审计。这个顺序不能颠倒。如果先调用后端再判断策略就失去意义了。第四mcp.server.stdio.stdio_server 负责建立 stdio 的读写流。如果你的 MCP SDK 版本较老可能没有这个接口可以参考官方示例改成 server.run(read_stream, write_stream, initialization_options) 的形式。5.6 编写模拟客户端验证创建 tests/demo_client.py# tests/demo_client.py import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client SERVER_PARAMS StdioServerParameters( commandpython, args[-m, proxy.proxy_server], cwd., ) async def main() - None: async with stdio_client(SERVER_PARAMS) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools (await session.list_tools()).tools print(代理暴露给模型端的工具) for t in tools: print(f - {t.name}: {t.description}) print(\n1) 允许调用: file_read workspace/notes.md) res await session.call_tool(file_read, {path: workspace/notes.md}) print(f isError{res.isError}, content{res.content}) print(\n2) 策略拦截: file_delete workspace/notes.md) res await session.call_tool(file_delete, {path: workspace/notes.md}) print(f isError{res.isError}) print(f content{res.content[0].text}) print(\n3) 策略拦截: file_write /tmp/hack.sh) res await session.call_tool( file_write, {path: /tmp/hack.sh, content: echo pwned}, ) print(f isError{res.isError}) print(f content{res.content[0].text}) print(\n4) 允许调用: file_write workspace/out.txt) res await session.call_tool( file_write, {path: workspace/out.txt, content: hello proxy}, ) print(f isError{res.isError}, content{res.content}) if __name__ __main__: asyncio.run(main())这个客户端脚本的任务就是模拟一个“不太守规矩的 LLM”。它会故意尝试调用后端存在但策略不允许的工具。6. 运行与验证6.1 准备测试文件在项目根目录下创建一个 workspace/notes.mdmkdir -p workspace echo MCP Policy Proxy 示例笔记 workspace/notes.md6.2 运行模拟客户端在项目根目录执行python tests/demo_client.py预期输出大致如下代理暴露给模型端的工具 - file_read: 读取文本文件内容。 - file_write: 将内容写入指定文本文件。 1) 允许调用: file_read workspace/notes.md isErrorFalse, content[TextContent(typetext, textMCP Policy Proxy 示例笔记\n)] 2) 策略拦截: file_delete workspace/notes.md isErrorTrue content[POLICY BLOCKED] 工具 file_delete 被策略拦截原因工具 file_delete 命中拒绝策略 3) 策略拦截: file_write /tmp/hack.sh isErrorTrue content[POLICY BLOCKED] 工具 file_write 被策略拦截原因工具 file_write 的参数不符合策略约束 4) 允许调用: file_write workspace/out.txt isErrorFalse, content[TextContent(typetext, text文件写入成功workspace/out.txt)]从输出中可以看到代理不是简单地“提示模型不要这么做”而是真的在后端工具被调用之前直接把请求拦截掉了。6.3 查看审计日志运行完成后logs/audit.jsonl 中会出现类似记录{ts: 2025-01-01T12:00:0000:00, event: TOOL_CALL, tool: file_delete, args: {path: workspace/notes.md}, allowed: false, reason: 工具 file_delete 命中拒绝策略} {ts: 2025-01-01T12:00:0100:00, event: TOOL_CALL_BACKEND, tool: file_read, args: {path: workspace/notes.md}, backend_is_error: false}审计日志的意义在于出现安全事故时你可以完整还原是哪一次调用、哪个参数、被谁触发、是否放行。7. 常见问题与排查思路7.1 代理启动后工具列表为空问题现象常见原因解决思路list_tools 返回空列表后端 MCP Server 启动失败单独运行后端脚本确认能否正常启动list_tools 返回空列表策略 allow 规则写错没有任何工具匹配检查 proxy.yaml 中 allow 的名称是否与后端工具名一致list_tools 返回空列表子进程路径不对检查 backend.args 中的脚本路径是否存在7.2 报错 Tool not found问题现象常见原因解决思路调用工具时报 Tool not found代理暴露的工具名与后端不一致检查后端工具名或确认策略未重命名工具调用工具时报 Tool not found后端工具列表被策略过滤检查 list_tools 返回结果确认工具是否真的可见7.3 策略参数校验不生效问题现象常见原因解决思路明明写了参数约束还是被放行arguments 中的参数名与工具定义不一致在后端工具中打印 arguments确认参数名明明写了参数约束还是被放行allowed_patterns 匹配逻辑太宽松在策略引擎中加日志观察匹配结果7.4 Windows 下路径匹配异常问题现象常见原因解决思路workspace/** 无法匹配 workspace\notes.mdWindows 路径分隔符是反斜杠在进入匹配前统一 replace(\\, /)绝对路径与相对路径混用策略中使用绝对路径调用时传相对路径统一规范路径或改用 os.path.abspath 归一化7.5 排查建议顺序如果你在集成时遇到其他问题建议按以下顺序排查先确认后端 MCP Server 能独立运行并枚举出工具。再确认代理进程能成功初始化后端连接。用简单策略例如只允许一个工具验证过滤逻辑。最后再逐步加参数约束和 deny 规则。全程查看 logs/audit.jsonl它会把每次调用的判断结果记录得很清楚。8. 最佳实践与工程建议8.1 使用“默认拒绝”策略生产环境建议优先使用 allowlist 模式而不是 denylist 模式。denylist 看起来省事但它要求你“枚举所有风险”而风险是枚举不完的。今天你只禁止了 file_delete明天新增一个 batch_delete 工具又需要补规则。allowlist 模式下所有工具默认不可见、不可调用。只有明确写进 allow 的工具才会被模型看到和调用这样新接入的工具默认就是安全的。8.2 隐藏工具名而不是只做拦截从安全角度看隐藏工具名比单纯拦截更有价值。当一个工具从 list_tools 结果中被过滤掉时模型端根本不知道它的存在因此不会生成调用意图。而如果工具仍然暴露只是调用时被拦截模型虽然失败一次但它可能换个参数继续尝试甚至试图绕过参数校验。隐藏高风险工具并将它们的描述从模型上下文中移除是从源头减少风险。8.3 参数校验要独立于工具描述不要依赖工具描述里写的“请传入 workspace 内路径”。描述再明确模型也可能传错或者在注入攻击下故意传错。参数校验必须由策略引擎独立完成。校验越严格模型的自由度就越低。你可以把参数校验理解为一个正则表达式级别的边界而不是一句“建议”。8.4 审计日志是刚性需求只要允许模型调用工具就必须有审计日志。日志需要记录以下信息调用时间。调用来源客户端标识。工具名。完整参数。策略决策结果。后端返回状态。如果是策略拦截还要记录匹配到的规则。另外要注意不要在日志中记录 API Key、Token、私钥等敏感字段。可以加脱敏函数例如把 secret 字段的值替换成掩码。8.5 多后端隔离当一个代理需要连接多个 MCP Server 时建议改为多后端配置backends: - name: file-server command: python args: [mcp_backend/backend_server.py] tools: - file_read - file_write - name: db-server command: python args: [mcp_backend/db_server.py] tools: - db_query代理在转发 tools/call 时需要根据工具名找到对应的后端会话。这样可以统一在一层做策略同时避免所有后端互相暴露。8.6 使用独立进程和最小权限策略代理必须与 LLM 主进程分离。如果策略和模型运行在同一个进程中那么模型参数一旦被注入理论上可能通过某些内部接口影响策略状态隔离性就差很多。生产环境建议代理使用独立系统账号运行。代理进程拥有最小文件权限。策略文件只允许管理员写入。审计日志目录独立挂载并做轮转备份。8.7 保留人工熔断能力策略引擎不一定要硬编码所有规则。可以给策略文件增加远程变更接口或者至少支持热加载。当出现突发事件时运维可以立即将某个工具全局禁用而不需要重新启动代理进程。例如在 scan 间隔内重新读取 YAML 文件或者监听配置更新信号kill -USR2 proxy_pid代理收到信号后重新加载策略文件即可实现动态策略更新。9. 总结与学习路线本文从“模型调用工具为什么需要强制策略”这个问题出发实现了一个完整的 LLM 与 MCP Server 之间的策略代理。它的核心价值不是给模型“讲道理”而是在执行链路上建立一道模型无法绕过的检查边界。通过这个项目你可以掌握三个关键能力MCP Server 与 MCP Client 的双向代理模型。工具列表的过滤与隐藏策略。基于参数约束的调用拦截与审计记录。接下来可以继续深入的方向有增加签名校验防止代理请求被重放。接入 OAuth2、API Key 等身份认证层。把策略引擎与外部权限系统联动。针对不同 LLM 客户端的工具调用习惯设计更细粒度的调用频率限制。如果你正在做 AI Agent 或 MCP 相关的安全体系建议先跑通本文这个最小示例然后逐步把策略规则、后端连接和审计日志接到自己的业务里。工具调用越自由策略代理就越有存在价值。