🛡️ 给 Agent 装一道安全门:从零手搓 DeepSeek + MCP + 安全层的生产级 Harness 设计与实现

一个从零实现的、可运行的 DeepSeek 工具调用 Agent 框架:通过 MCP 协议 接入文件/天气/命令/知识库等工具,在每次工具调用前用 三层安全检查 把关,并附带 RAG 知识库检索、上下文压缩、测评闭环与离线分析

代码以 src/ 下的标准 Python 包 aharness 组织,按 mcp / security / test / utils 子包拆分,可 pip install -e . 后以 python -m aharness.* 运行。


在这里插入图片描述

一、本示例在构建什么?

大语言模型本身没有"动手能力"——它只能生成文本。要让 Agent 真正做事(查天气、读文件、调数据库),必须把它接入外部工具。而工具接入带来一个核心矛盾:

能力越大,风险越大。 一个能执行命令、删除文件的 Agent,如果没有任何约束,就是一把没有保险的枪。

本项目想解决的就是这个问题。它把 Agent 的"工具调用"这一核心环节,用一套可插拔的安全中间件包裹起来:

用户问题 → LLM 推理 → 模型想调用工具
                          │
                    ┌─────▼─────┐
                    │  安全层    │ ← 3 步检查,任一失败即拦截
                    └─────┬─────┘
                          │ 放行
                    ┌─────▼─────┐
                    │  MCP 工具  │ ← 真正的工具执行
                    └───────────┘

配合 RAG 知识库(让模型"先检索再回答")、上下文压缩(长对话不掉 token 成本)、轨迹记录(每次运行全程可审计),构成一个相对完整的 Agent 生产框架雏形。


二、架构总览:可插拔架构 + 标准包结构

代码全部收敛在 src/aharness/ 包中,按功能拆成 4 个子包 + 2 个根模块。安全层是中间件、策略是纯数据对象、审批门可替换、压缩器可替换。每个模块都可以单独拿出来测试或替换实现。

src/aharness/
├── agent_deepseek_v3.py      # 入口:CLI 解析 + 按模式运行(最薄)
├── agent_runner.py           # Agent 主循环编排,run_agent()(核心)
│
├── mcp/
│   ├── mcp_client.py         # MCP 客户端:MCPClient + MultiMCPManager
│   ├── mcp_server_with_delete.py # MCP Server①:天气/文件/命令 6 个工具
│   └── mcp_rag_server.py     # MCP Server②:RAG 知识库 3 个工具
│
├── security/
│   ├── security_policy.py    # 安全策略:SecurityPolicy + 三种模板工厂
│   ├── security_layer.py     # 安全层中间件:check_tool_call()(拦截点)
│   ├── parameter_validator.py# 参数校验:ToolValidator + 敏感内容扫描
│   └── human_approval_gate.py# 人工审批门:HumanApprovalGate
│
├── test/
│   ├── test_scenarios.py     # 10 个测试场景的结构化定义(数据驱动)
│   ├── run_all_tests.py      # 全套 10 场景 runner(子进程隔离)→ 生成报告
│   ├── run_single.py         # 单个场景入口(被 run_all_tests 以子进程调用)
│   ├── quick_test.py         # 快速跑单个场景(不跑全套)
│   ├── report_generator.py   # 从 trace 生成 Markdown 对比报告
│   └── check_env.py          # 运行前环境检查
│
└── utils/
    ├── config.py             # 地基:Windows asyncio 修复 / .env / 颜色打印 / PROJECT_ROOT
    ├── trace_logger.py       # 轨迹记录:TraceLogger(JSONL + 统计)
    ├── context_compressor.py # 上下文压缩:ContextCompressor
    ├── analyzer.py           # 离线分析:analyze_trace()
    ├── seed_knowledge.py     # 向 RAG 知识库写入种子数据
    └── fix_encoding.py       # Windows 控制台强制 UTF-8(GBK 乱码修复)

运行产物统一落在项目根目录(由 config.pyPROJECT_ROOT 常量统一锚定,不随进程 cwd 变化):

harnessDEV/
├── pyproject.toml            # 打包配置(pip install -e . 后可直接 import aharness)
├── src/aharness/             # 上述包结构
├── data/                     # RAG 知识库 SQLite(rag_knowledge.db)
├── sandbox/                  # 文件类工具的沙箱目录
├── traces/                   # 运行轨迹 JSONL(每次运行自动生成)
└── reports/                  # 测试对比报告(run_all_tests 自动生成)

依赖方向(无循环导入,config 是纯地基):

config → 全部模块(被反向消费)
security_policy / parameter_validator / human_approval_gate → security_layer
security_layer → agent_runner
trace_logger → { security_layer, context_compressor, agent_runner }
mcp_client → agent_runner

三、核心闭环:run_agent() 一次完整的数据流

agent_runner.py 是整个框架的心脏。一次 run_agent() 调用,完整经历 8 个阶段

user_query
    │
    ▼
[1] 启动两个 MCP Server 子进程(stdio 传输)
    │    aharness.mcp.mcp_server_with_delete + aharness.mcp.mcp_rag_server
    ▼
[2] 拉取 9 个工具 → 注册到安全层 → 按白名单过滤
    │    只把"可见工具"暴露给模型(从源头减少危险尝试)
    ▼
[3] 第①次调 LLM(thinking 模式 + 可见工具)
    │
    ├─ 模型未请求工具 → [直接输出最终回答,结束]
    │
    └─ 模型请求工具 → [4] 逐条过安全层 check_tool_call()  ← ★ 拦截点
              │ 放行 → MultiMCPManager 路由到对应 Server 执行
              └ 拒绝 → 把拒绝原因回填给模型,跳过执行
    ▼
[5] 上下文压缩(消息超阈值时摘要早期消息)
    ▼
[6] 第②次调 LLM(携带工具结果)→ 输出最终回答
    ▼
[7] TraceLogger.print_summary() 统计摘要 + trace 落盘
    ▼
[8] finally: 关闭全部 MCP Server 子进程

关键代码结构(agent_runner.py:99):

async def run_agent(user_query: str, policy: SecurityPolicy, auto_approve: bool = True):
    # 评测闭环之轨迹记录
    logger = TraceLogger()
    logger.log_event("run_start", {"query": user_query, "model": policy.mode})

    # 多 MCP 管理器:按「模块路径」注册两个 Server
    mcp_manager = MultiMCPManager()
    await mcp_manager.add_server("with_delete", "aharness.mcp.mcp_server_with_delete")
    await mcp_manager.add_server("rag", "aharness.mcp.mcp_rag_server")

    # 上下文压缩器(token 估算阈值 8000)
    compressor = ContextCompressor(client, MODEL, threshold=8000)

    # 启动 MCP 服务,拉取工具
    openai_tools, tool_schemas = await mcp_manager.start_all()

    # 初始化安全层组件
    validator = ToolValidator(tool_schemas)
    approval_gate = HumanApprovalGate(auto_approve=auto_approve)
    security = SecurityLayer(policy, validator, approval_gate, logger)

    # 只把"可见工具"(白名单内)暴露给模型 —— 从源头减少危险尝试
    visible_tools = [t for t in openai_tools
                     if policy.is_tool_allowed(t["function"]["name"])[0]]

    # 同步 OpenAI SDK 丢进线程池,避免阻塞事件循环
    with concurrent.futures.ThreadPoolExecutor() as pool:
        response = await loop.run_in_executor(pool, lambda: call_llm(messages, visible_tools))

    # ... 对每个 tool_call:
        # ★ 拦截点
        allowed, reason = await security.check_tool_call(func_name, args)
        if allowed:
            result = await mcp_manager.call_tool(func_name, args)          # 路由执行
        else:
            messages.append({"role": "tool", ...,
                            "content": f"🔒 安全策略拒绝执行: {reason}"})  # 原因回填

几个值得注意的实现细节:

  1. 同步 SDK 塞进线程池:OpenAI SDK 是同步 API,直接 await 会阻塞事件循环,所以用 run_in_executor 丢进 ThreadPoolExecutor
  2. 拒绝原因回填给模型:安全层拦截不是"静默吞掉",而是把"为什么不能做"告诉模型,让模型据此给出正确回复。
  3. finally 关闭 Server:无论成功失败都 stop_all(),避免残留子进程。
  4. Windows 事件循环config.py 在模块导入期强制 WindowsSelectorEventLoopPolicy,解决 stdio 与默认 Proactor 事件循环的兼容问题。

四、工具接入:MCP 协议 + 多 Server 路由

MCP(Model Context Protocol)是 Anthropic 提出的标准化工具协议。这里用的是 stdio 传输:客户端用 python -m aharness.mcp.<server> 拉起子进程,通过 stdin/stdout 走 JSON-RPC 通信。

MultiMCPManager 解决了"多个工具服务器并存"的问题:

  • 统一工具发现start_all() 并行拉起所有 Server,把每个 Server 的 MCP 工具转成 OpenAI 兼容的 tools 格式{"type":"function","function":{...}}),无缝喂给 DeepSeek。
  • 路由表:维护 tool_name → server_name 映射,调用时自动路由到正确的子进程。
  • 工具名冲突检测:同名工具出现在多个 Server 时打印告警,后注册的覆盖先注册的。
  • 防御性解析:某些实现 resp.tools 是嵌套列表,代码做了拍平处理。

两个内置 Server 共提供 9 个工具

# 工具 来源 Server 风险等级 说明
1 get_weather ① 文件/天气/命令 🟢 LOW 获取城市当前天气
2 get_forecast 🟢 LOW 天气预报(1-7 天)
3 read_file 🟡 MEDIUM 读取沙箱文件
4 write_file 🟡 MEDIUM 写入沙箱文件
5 delete_file 🟠 HIGH 删除沙箱文件 ⚠️ 需 confirm
6 execute_command 🔴 CRITICAL 执行系统命令 ⚠️⚠️
7 search_documents ② RAG 知识库 🟢 LOW 知识库检索(优先调用)
8 add_document 🟠 HIGH 向知识库写入文档
9 list_sources 🟡 MEDIUM 列出知识库来源

两个 Server 以模块路径注册("aharness.mcp.mcp_server_with_delete" / "aharness.mcp.mcp_rag_server"),由 MCPClientpython -m 拉起子进程;Server 内部从 config.py 读取 PROJECT_ROOT,把 sandbox/data/ 锚定到项目根目录,不再依赖进程 cwd。


五、安全层:给 Agent 上的三道锁

这是本框架的核心亮点SecurityLayer.check_tool_call() 是安全层唯一入口,一次工具调用依次经过 3 步检查(任一失败立即拦截):

Step 1  工具治理     → SecurityPolicy.is_tool_allowed()
                        黑名单直接拒绝;白名单非空且不在其中则拒绝
Step 2  参数校验     → ToolValidator.validate()
                        JSON Schema → 自定义约束 → 敏感内容扫描
Step 3  风险审批     → HumanApprovalGate.approve()
                        LOW/MEDIUM → 自动放行(auto_approve 时)
                        HIGH/CRITICAL → 弹交互式 CLI 审批(y/N)
                        UNKNOWN(未登记工具)→ 一律拒绝(fail-safe)

第 1 步:工具治理

security_policy.py 定义了黑名单优先、白名单兜底的判定。核心哲学是默认拒绝:未登记的工具风险等级是 UNKNOWN,上游一律拦截——宁可误杀,不可放过。

第 2 步:参数校验(三道子防线)

parameter_validator.py 的参数校验本身又是三层:

  1. JSON Schema 校验:按 MCP 工具自带的 inputSchema 校验类型/必填/范围(jsonschema 库,可选依赖,未安装则降级跳过)。
  2. 自定义业务约束:如 get_forecast.days ∈ [1,7]write_file.content ≤ 10000 字符,并处理了 bool is int 这个经典 Python 陷阱。
  3. 敏感内容扫描SensitivePatternScanner 用正则识别 5 类攻击特征
攻击类别 典型模式
SQL 注入 DROP TABLEOR 1=1-- 注释符
路径穿越 ..//etc/、URL 编码的 %2e%2e/
SSRF 内网探测 127.0.0.1192.168.*file://gopher://
命令注入 ;rm$(...)、反引号、&&、`
密钥泄露 sk- 开头长串、AWS AKIA、明文 password=

一个隐蔽的细节:参数以 JSON 形式传来时,换行会被序列化成 \n,直接扫原始串可能漏掉 \n rm 这类换行注入。所以扫描器对同一份参数做了两次扫描——原始 JSON 串 + unicode_escape 还原后的真实字符串。

第 3 步:风险审批

human_approval_gate.py 按风险等级分流:

  • LOW/MEDIUMauto_approve 时自动放行(开发便利)
  • HIGH/CRITICAL → 弹交互式 CLI:展示工具名/风险/参数,等待用户 y/N
  • UNKNOWN → 一律拒绝(fail-safe)

实现细节:input() 是阻塞的同步调用,直接 await 会冻住事件循环,所以放进 ThreadPoolExecutor;审批异常/超时也按拒绝处理——所有失败路径都倒向安全

纵深防御:服务端还有第二道防线

安全层是客户端第 1 道防线。MCP Server 内部(mcp_server_with_delete.py)还有服务端第 2 道防线

  • 路径沙箱 _resolve_safe_path():拒绝一切含路径分隔符/../~/$/|/;/& 的输入,再用 normpath 归一化校验目标必须仍在 sandbox/ 内。
  • delete_file 二次确认:必须显式传 confirm=true 才真正删除。
  • 受保护文件important.txtconfig.ini 服务端直接拒绝删除。
  • execute_command 白名单:仅 ls/dir/pwd/echo/date/whoami/cat 等只读命令,且过滤管道/重定向/子 shell 等危险字符,10s 超时。

每一层都有明确的边界,攻击者需要同时击穿"客户端 3 步检查"和"服务端多道防线"才可能造成破坏。


六、RAG 知识库:让模型"先检索,再回答"

mcp_rag_server.py 实现了一个 零依赖(仅标准库 + SQLite)的 RAG

  • 存储:SQLite FTS5 全文索引虚拟表,tokenize='porter unicode61' 支持中英文分词;doc_meta 表存来源与入库时间。数据库固定落在 <项目根>/data/rag_knowledge.db(环境变量 RAG_DB_PATH 可覆盖)。
  • 检索:FTS5 MATCH + BM25 相关性排序 + snippet() 高亮截取片段。
  • 三个工具add_document(写库)、search_documents(检索)、list_sources(列来源)。

关键设计在 agent_runner.pySystem Prompt 里——引导模型"知识库优先检索":

  • 当用户问题涉及「之前讨论过的内容」「知识库中的信息」时,必须首先调用 search_documents,即使你觉得自己可能知道答案。
  • 如果 search_documents 返回了相关结果,必须基于检索结果回答。
  • 如果返回「未找到」,再结合自身知识回答,并明确告知用户。

先灌入种子数据(seed_knowledge.pypython -m aharness.utils.seed_knowledge),Agent 就能回答"上海适合跑步的月份"这类需要检索的问题。


七、上下文压缩:长对话的 token 成本控制

context_compressor.py 在消息量超过阈值(代码里配置 8000 tokens)时自动触发,三策略组合:

  • A. Rolling Summary:把早期对话轮次交给 LLM 生成 ≤300 字中文摘要,替换原文
  • B. Keep-Recent-N:始终保留最近 6 条完整对话不被压缩
  • C. System Prompt 永不压缩

实现里同样有"同步 SDK 进线程池"和"压缩失败降级返回原消息"(保证不丢上下文、不中断主流程)两个细节。token 估算用「字符数 ÷ 3」粗略近似(中文约 1.5~2 字符/token、英文约 4 字符/token),注释里明确说明生产环境应换 tiktoken 精确计算。


八、测评闭环与离线分析:让 Agent 可观测、可复盘

trace_logger.py 把每次运行写成一个 JSONL 文件(traces/trace_<时间戳>.jsonl),每行一个事件。8 类事件:

事件 含义
run_start / run_end 运行开始 / 结束(含最终回答)
tools_list 全部工具 / 模型可见工具
llm_response 模型返回(finish_reason / 思考预览 / usage)
tool_call 工具调用(请求 + 结果 / 错误)
compression 上下文压缩(前后 token / 消息数)
security 安全事件(放行/拒绝/校验失败/审批)
error 主流程异常

运行结束自动打印统计摘要:LLM 调用次数、token 消耗、工具成功率、安全拦截/审批次数、耗时,以及完整的安全审计清单。

离线复盘用 analyzer.py

from aharness.utils.analyzer import analyze_trace
analyze_trace("traces/trace_20260814_222001.jsonl")

全套测试由 run_all_tests.py 一键驱动:依次以独立子进程跑完 10 个场景(MCP 生命周期完全隔离,单场景崩溃不影响后续),再由 report_generator.py 生成对比报告到 <项目根>/reports/


九、开箱即用的三种安全策略模板

SecurityPolicyFactory 提供三种模板,对应不同信任场景:

模板 mode 白名单 黑名单 适用场景
development() development 无(全放行) execute_command 本地开发,配合 --auto-approve
production() production get_weather, get_forecast, read_file, search_documents, list_sources execute_command, delete_file, add_document 生产环境
strict_whitelist() strict get_weather, get_forecast, list_sources 其余全部 受限环境

用法:安全策略是纯数据对象,可序列化、可热替换、可单独单元测试——这正是把它独立成模块的价值。


十、快速开始

# 1. 创建并激活虚拟环境
python -m venv venv
venv\Scripts\activate.bat        # Windows;退出用 deactivate

# 2. 安装依赖 + 可编辑安装本项目(pyproject.toml)
pip install -U openai mcp jsonschema
pip install -e .                 # 使 src/ 下的 aharness 可导入,之后即可 python -m aharness.*

# 3. 设置 API Key(环境变量 或 .env 文件,优先级:环境变量 > .env)
#    Windows CMD:   set DEEPSEEK_API_KEY=sk-your-real-key
#    PowerShell:    $env:DEEPSEEK_API_KEY="sk-your-real-key"
#    Linux/Mac:     export DEEPSEEK_API_KEY=sk-your-real-key

# 4. (可选)环境检查
python -m aharness.test.check_env

# 5. 先灌入 RAG 种子数据(否则 search_documents 查不到内容)
python -m aharness.utils.seed_knowledge

# 6. 运行 Agent(默认开发模式)
python -m aharness.agent_deepseek_v3 --mode dev --auto-approve

# 7. 指定模式 / 自定义问题
python -m aharness.agent_deepseek_v3 --mode prod
python -m aharness.agent_deepseek_v3 --mode strict --query "北京今天天气怎么样?"
python -m aharness.agent_deepseek_v3 --mode all        # dev + prod + strict 依次跑

# 8. 全套 10 场景量化测试 + 生成对比报告(报告落在 reports/)
python -m aharness.test.run_all_tests

不想 pip install -e . 时,也可设置 PYTHONPATH=src 后运行同样命令(等价,只是每次都要带环境变量)。

三个内置测试场景(对应三种安全策略,演示安全层的差异化拦截):

模式 默认问题 预期行为
dev 删除 sandbox 里的 notes.md delete_file 可放行(HIGH → 人工审批)
prod 查上海跑步月份 + 删 important.txt search_documents 放行;delete_file 白名单外被拒
strict 北京天气 仅只读工具可用

十一、10 个测试场景 & 一次真实运行日志

全部场景以数据驱动方式定义在 test_scenarios.py:每场景是一个 dict(id / 名称 / query / mode / auto_approve / 预期工具 / 预期安全事件 / 分类),加新测试只需追加一个 dict:

# 场景 分类 预期
1 RAG 优先检索 rag search_documents 放行
2 多工具组合(天气 + RAG) rag get_weather + search_documents 放行
3 黑名单直接拒绝(execute_command) security 拒绝
4 生产模式白名单外拒绝(delete_file) security 拒绝
5 敏感内容扫描(SQL 注入) security 参数校验失败
6 路径穿越攻击 security 参数校验失败
7 HIGH 风险自动审批放行 approval delete_file 放行
8 生产模式写知识库被拒(add_document) security 拒绝
9 严格模式只读放行 baseline get_weather 放行
10 无工具调用基线(纯对话) baseline 直接回答

想单跑某一个场景(不跑全套)时用 quick_test.py

python -m aharness.test.quick_test 7          # 只跑第 7 个场景(HIGH 审批)
python -m aharness.test.quick_test 3 --no-approve

traces/trace_*.jsonl(dev 模式,--auto-approve)为例,看安全层如何工作:

// ① 模型请求删除 notes.md(HIGH 风险工具)
{"event": "llm_response", "finish_reason": "tool_calls",
 "reasoning_preview": "This is a high-risk operation that requires confirmation..."}

// ② 安全层 3 步检查全部通过(工具放行 → 参数合规 → HIGH 经人工审批放行)
{"event": "security", "action": "security_approved",
 "tool": "delete_file", "detail": "risk=HIGH", "policy_mode": "development"}

// ③ 工具真正执行,结果回填
{"event": "tool_call", "tool": "delete_file",
 "args": {"filename": "notes.md", "confirm": true},
 "result": "🗑️ 已删除: notes.md", "success": true}

// ④ 二次调用 LLM,输出最终回答
{"event": "llm_response", "finish_reason": "stop", ...}
{"event": "run_end", "status": "success",
 "answer": "✅ 已成功删除沙箱中的 `notes.md` 文件。..."}

这个例子展示了完整闭环:模型想删文件 → 安全层判定 HIGH → 人工确认 → 放行执行 → 结果回填 → 二次回答。全程 6 个事件可审计。


十二、设计取舍与踩坑总结

  1. “先拒绝再放行"优于"先放行再拦截”:从工具可见性(Step 1 就不让模型看到危险工具)到审批(UNKNOWN 一律拒绝),所有不确定都倒向安全。
  2. 异步里的同步阻塞:Windows 的 ProactorEventLoop 与 stdio 混用会炸,config.py 在模块导入期强制切换 WindowsSelectorEventLoopPolicy;所有 input() 和 OpenAI SDK 调用都丢进线程池。
  3. 每一层都要有最后防线:客户端安全层可能被绕过,所以 Server 端还有路径沙箱/确认标志/命令白名单——纵深防御不是摆设。
  4. 审计是安全的一部分:没有轨迹记录的安全层无法复盘;所有安全决策都进 trace,才能回答"刚才那步为什么放行了"。
  5. System Prompt 也是安全设计:与其事后拦截 delete,不如引导模型优先检索知识库、明确告诉它哪些操作受限。
  6. 路径全部锚定项目根目录data / sandbox / traces / reportsconfig.pyPROJECT_ROOT 统一定义,子进程无论 cwd 在哪,产物都落在根目录,可重复复现。
  7. Windows 控制台 GBK 乱码fix_encoding.py 在模块导入期把 stdout/stderr 强制包装为 UTF-8,agent_runner 已 import 它,规避 emoji/中文 print 崩溃。

十三、已知问题与后续方向

  • RAG 检索精度char÷3 估算 token 是近似值,生产应换 tiktoken;FTS5 + BM25 对语义检索能力有限,后续可接入 embedding 向量检索。
  • 审批交互形态:目前 HIGH 风险走 CLI 交互,生产环境可扩展为 Web UI / Slack / Webhook 异步审批(HumanApprovalGate 的接口已为此预留)。
  • 安全规则静态化:正则扫描无法覆盖语义级攻击,可结合 LLM-as-judge 做二次判断。
  • 可扩展方向:长期记忆、Subagent 编排、自进化 Agent、多模型路由。

License

仅供学习与安全测试场景使用。请勿将内置的 execute_command / delete_file 等高风险工具直接暴露给不可信输入。

安全提示:任何真实 API Key 都应通过环境变量注入,绝不要提交进代码仓库。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐