1. 为什么局域网里需要一个“不联网”的 AI Agent 平台?
最近在某高校实验室做边缘智能项目时,团队反复被同一个问题卡住:想让几个学生用 DeepSeek 的模型跑 Agent 流程做课程设计,但一开 WebUI 就弹出“需联网验证 token”;本地部署的 Ollama 模型又不支持多 step 的 tool calling 和 memory 管理;自己手写 LangChain 脚本?三天写完,两天调试,一天改 bug——最后发现连最基础的“调用本地 Python 工具查天气”都得手动 mock 返回值。
这时候我翻到 DeepSeek 官方 GitHub 上那个被星标 3200+ 次的仓库:DeepSeek-Harness。它不是模型,也不是 UI,而是一个极轻量、纯 Python 实现的Agent Runtime 核心框架——没有前端、不依赖云服务、不强制注册账号、所有调度逻辑都在本地进程内闭环。更关键的是,它的设计哲学非常“老派务实”:只管三件事——接收用户 query、按 plan 调用工具(可以是 shell 命令、Python 函数、HTTP 接口)、把结果组装成自然语言返回。不碰模型加载,不搞分布式训练,不卷 RAG 架构,就专注把“Agent 的骨架”搭稳。
而 Docker 的价值,恰恰在于把这种“骨架”变成可复制、可隔离、可迁移的标准化单元。我们不需要在每台学生电脑上装 Python 环境、配 conda channel、解决 protobuf 版本冲突;也不用担心某人升级了 PyTorch 导致 harness 启动失败;更不用为“为什么我的 Mac 跑得好好的,Windows 同学却报错 OSError: [WinError 193]”这种问题开两小时线上会议。
所以,“Docker 部署 DeepSeek Harness”这件事的本质,不是为了炫技,而是为了解决一个真实场景下的三个刚性需求:
- 环境一致性:同一份配置,在 Ubuntu 服务器、MacBook M3、甚至树莓派 5 上启动后行为完全一致;
- 网络可控性:所有通信走局域网 IP + 端口,不发任何请求到公网,模型权重和工具代码全在内网流转;
- 职责分离性:Harness 只做调度,模型由 vLLM 或 Ollama 托管,工具由独立服务提供,彼此通过 HTTP/IPC 解耦——这才是真正可维护的 Agent 架构。
提示:很多人误以为“部署 Agent 就是部署大模型”,其实恰恰相反。DeepSeek-Harness 的核心价值,是在模型之上加一层“可编程的操作系统”。就像 Linux 不等于 CPU,Harness 也不等于 DeepSeek 模型——它让你能用 YAML 写“Agent 程序”,而不是靠 prompt 工程硬凑。
我试过直接 pip install deepseek-harness 在本地跑 demo,确实 5 分钟就能看到效果。但一旦加入自定义工具(比如调用本地 SQLite 查课程表、执行 curl 获取教务系统课表接口),环境依赖就开始爆炸式增长。而用 Docker,你只需要在Dockerfile里写清楚:“我要 Python 3.11、requests、pydantic、sqlite3、以及我自己的 tools/ 目录”,剩下的交给镜像构建引擎。这背后省掉的,是数不清的“pip list 对比”和“requirements.txt 版本锁死”。
2. DeepSeek-Harness 的真实能力边界与常见误解
在动手写 Dockerfile 之前,必须先厘清 DeepSeek-Harness 到底“能做什么”和“坚决不做什么”。官方文档写得克制,但社区里大量失败案例,根源都是对它的定位理解偏差。
2.1 它不是模型服务,而是 Agent 编排器
这是最容易踩坑的一点。DeepSeek-Harness 自身完全不加载模型、不处理 tokenization、不实现 inference。它只做三件事:
- 接收结构化输入:一个 JSON payload,包含
query(用户问题)、tools(可用工具列表)、model_endpoint(指向外部模型服务的 URL); - 执行 Plan-Execute-Observe 循环:调用 LLM 生成 tool call → 解析参数 → 调用对应工具 → 收集返回 → 再次提交给 LLM → 直到生成最终 answer;
- 返回结构化输出:包含
final_answer、steps(每步的 tool name + input + output)、tool_calls(原始调用记录)。
这意味着:你必须提前准备好一个运行中的模型服务。它可以是:
- vLLM 托管的 DeepSeek-V2-Chat(推荐,吞吐高、支持 streaming);
- Ollama 运行的 deepseek-coder:6.7b(适合低配设备);
- 甚至是你自己用 Transformers + Flask 封装的简易 API(只要返回符合 OpenAI 兼容格式的 response)。
注意:Harness 默认使用 OpenAI 兼容的
/v1/chat/completions接口。如果你用的是非标准 API(比如返回字段叫result而不是choices[0].message.content),就必须修改harness/llm/client.py里的解析逻辑——这不是 bug,是设计使然:Harness 把模型抽象成“黑盒函数”,你负责保证输入输出契约。
2.2 它不内置工具,但提供了最灵活的工具注册机制
很多教程教你“如何用 Harness 调用天气 API”,然后贴一段weather_tool.py代码。这没错,但没说透本质:Harness 的工具系统,本质是一个 Python 函数注册表 + YAML 描述层。
你只需做两件事:
- 在
tools/目录下写一个 Python 文件,比如db_query.py,里面定义一个带@tool装饰器的函数:from harness.tool import tool @tool def query_course_db(course_name: str) -> str: """Query local SQLite database for course information""" import sqlite3 conn = sqlite3.connect("/data/courses.db") cursor = conn.cursor() cursor.execute("SELECT teacher, time FROM courses WHERE name LIKE ?", (f"%{course_name}%",)) result = cursor.fetchall() conn.close() return str(result) - 在
tools/tools.yaml中声明该工具的元信息:query_course_db: description: "Query local SQLite database for course information" parameters: course_name: type: string description: "Name of the course to search"
Harness 启动时会自动扫描tools/目录,加载所有@tool函数,并根据 YAML 补充参数校验逻辑。整个过程不依赖任何数据库、不强制用 FastAPI、不绑定特定序列化协议——就是纯 Python 函数调用。
我实测过,在同一台机器上,让 Harness 同时调用三个不同来源的工具:
shell_tool.py:执行df -h命令获取磁盘空间;http_tool.py:向内网另一台 Flask 服务发 POST 请求查课表;python_tool.py:直接调用本地 NumPy 计算矩阵特征值。
它们共存毫无压力,因为 Harness 对工具的唯一要求是:能被 Python import,有明确的输入输出类型注解,且返回 JSON-serializable 数据。
2.3 它不提供 Web UI,但留出了最干净的集成入口
官方仓库里没有frontend/目录,也没有npm run dev。这不是疏忽,而是刻意为之。Harness 的设计者认为:Agent 的交互界面,应该由业务场景决定——可能是命令行脚本、可能是企业微信机器人、可能是嵌入到 Jupyter Notebook 的小部件、也可能是学生自己写的 Electron 桌面应用。
它只暴露两个核心接口:
- HTTP API:
POST /v1/agents/run,接收 JSON 输入,返回 JSON 输出; - CLI 工具:
harness run --query "帮我查人工智能导论的上课时间",适合调试和自动化。
这就意味着:如果你想在局域网里快速搭一个“学生能用浏览器访问的 Agent 平台”,最佳实践不是去魔改 Harness,而是另起一个轻量 Web 服务(比如用 Flask 或 FastAPI),在它的后端调用 Harness 的 HTTP API。这样做的好处是:
- Web 层可以加登录鉴权(LDAP 或简单 session);
- 可以记录每次调用日志到本地文件或 SQLite;
- 可以限制每个 IP 每分钟最多调用 5 次,防滥用;
- 前端完全自由:Vue、React、甚至纯 HTML + JS 都能对接。
我在某公司内部知识库项目中就采用这个模式:前端是 Vue3 单页应用,后端是 80 行 Flask 代码,它只做一件事——接收用户输入,拼装成 Harness 要求的 JSON,转发给http://harness:8000/v1/agents/run,再把 response 渲染成对话流。整个系统上线后,运维同事说:“比维护一个 WordPress 插件还省心。”
3. 从零构建可复用的 Docker 镜像:一份经生产验证的 Dockerfile
现在进入实操环节。下面这份Dockerfile,是我过去半年在三个不同项目中反复迭代、压测、修复后沉淀下来的稳定版本。它不追求最小体积(alpine 会带来 glibc 兼容性问题),也不堆砌花哨功能(比如自动拉取最新模型),而是聚焦于一次构建、处处运行、故障可追溯。
# 使用官方 Python 基础镜像,版本锁定为 3.11-slim-bullseye # slim-bullseye 是 Debian 11,glibc 版本稳定,兼容绝大多数科学计算包 FROM python:3.11-slim-bullseye # 设置工作目录,所有操作在此路径下进行 WORKDIR /app # 复制 requirements.txt 并安装系统级依赖 # 注意:这里只装 harness 本身依赖,模型服务和工具依赖另行处理 COPY requirements.txt . RUN apt-get update && apt-get install -y \ # libpq-dev 是 psycopg2 编译所需,即使你不用 PostgreSQL,也建议保留 libpq-dev \ # gcc 是编译 numpy/scipy 的必需项,slim 镜像默认不带 gcc \ # curl 用于后续健康检查脚本 curl \ && rm -rf /var/lib/apt/lists/* # 安装 Python 依赖,使用 --no-cache-dir 加速构建,--find-links 指向私有源(可选) RUN pip install --no-cache-dir -r requirements.txt # 创建非 root 用户,提升安全性 # UID 1001 是常见非特权用户 ID,避免容器内进程以 root 身份运行 RUN groupadd -g 1001 -f user && useradd -S -u 1001 -U -m -d /home/user user USER 1001 # 复制 harness 源码(假设你已 fork 并打上 patch) # 生产环境强烈建议指定 commit hash,而非用 main 分支 COPY --chown=1001:1001 . . # 复制自定义工具目录,保持权限一致 COPY --chown=1001:1001 tools/ tools/ # 暴露 Harness 默认端口(8000),便于 docker run -p 映射 EXPOSE 8000 # 健康检查:每 30 秒用 curl 检查 /health 端点,超时 3 秒,失败 3 次即标记为 unhealthy HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8000/health || exit 1 # 启动命令:使用 uvicorn,开启 reload 仅用于开发,生产环境务必关闭 # --host 0.0.0.0 是必须的,否则容器内无法被其他容器访问 CMD ["uvicorn", "harness.main:app", "--host", "0.0.0.0:8000", "--port", "8000", "--workers", "2"]3.1 关键参数选择背后的工程权衡
这份 Dockerfile 里每一行都不是随意写的,背后都有实际踩坑经验:
为什么用
python:3.11-slim-bullseye而不是alpine?
Alpine 使用 musl libc,而很多 Python 包(尤其是带 C 扩展的 numpy、pandas、protobuf)在 musl 下编译异常或运行时崩溃。我们曾在一个树莓派项目中用 alpine,结果harness run命令执行到第 7 次就 segmentation fault。换成 bullseye 后,连续运行 72 小时无异常。为什么
apt-get install里包含libpq-dev和gcc?
即使你当前不用 PostgreSQL,psycopg2是 Python 生态中最稳定的数据库驱动,很多自定义工具会依赖它。而gcc是编译numpy的必需项——slim 镜像默认不带编译器,pip install numpy会回退到纯 Python 版本,性能下降 5 倍以上。实测显示,加了gcc后,import numpy速度从 1.2 秒降到 0.15 秒。为什么
HEALTHCHECK用curl而不是nc?nc(netcat)只能检测端口是否开放,但无法判断应用是否 ready。Harness 启动后,可能还要加载工具、连接模型服务,此时端口已 open,但/v1/agents/run仍会返回 503。而/health端点是 Harness 内置的,它会检查工具加载状态和模型 endpoint 连通性。用curl能真正反映服务健康度。为什么
CMD里--workers 2?
Harness 是 CPU-bound 任务(解析 JSON、调用工具、拼接字符串),不是 I/O-bound。单 worker 在高并发下容易阻塞。测试数据显示:2 workers 时,QPS 从 8 提升到 22;4 workers 时 QPS 反而降到 19(GIL 争用加剧)。所以 2 是性价比最优解。
3.2 requirements.txt 的精简之道
requirements.txt是镜像大小和启动速度的关键。以下是经过裁剪的最小可行集合(基于 harness v0.3.2):
# DeepSeek-Harness 核心依赖 fastapi==0.115.0 uvicorn[standard]==0.32.0 pydantic==2.9.2 jinja2==3.1.4 # 工具开发常用依赖(按需添加,此处为通用场景) requests==2.32.3 python-dotenv==1.0.1 # 日志和监控(可选,但强烈建议) loguru==0.7.2注意两点:
- 绝不写
deepseek-harness==*或deepseek-harness>=0.3.0。必须锁定具体版本号,如git+https://github.com/deepseek-ai/harness.git@v0.3.2#egg=deepseek-harness。否则某天上游发布 v0.4.0,你的镜像构建就会失败。 - 删除所有
dev依赖:black、pytest、mypy这些只在开发机上需要,放进生产镜像是巨大浪费。一个black包会让镜像增大 42MB。
我统计过:用宽松版本号 + dev 依赖的镜像,大小 842MB;用锁定版本 + 精简依赖后,降至 317MB,构建时间从 6 分 23 秒缩短到 1 分 48 秒。
3.3 构建与推送:一条命令生成可部署资产
构建命令不是简单的docker build -t harness .,而是带上下文控制的完整流程:
# 1. 清理旧镜像,避免缓存污染 docker system prune -f # 2. 构建,指定平台(确保在 Apple Silicon 上构建的镜像也能在 x86 服务器运行) docker build --platform linux/amd64 -t harness:0.3.2-v1 . # 3. 推送到私有 registry(假设你的内网有一台 Harbor 服务) docker tag harness:0.3.2-v1 harbor.internal.local/ai/harness:0.3.2-v1 docker push harbor.internal.local/ai/harness:0.3.2-v1提示:
--platform linux/amd64是关键。很多团队在 MacBook 上构建镜像,然后推送到 Intel 服务器,结果容器启动报错exec format error。这是因为 Apple Silicon 默认构建 arm64 镜像,而大多数服务器是 amd64。加上此参数,Docker 会模拟 amd64 环境构建,确保二进制兼容。
4. 局域网多容器协同实战:用 docker-compose 组装完整 AI Agent 平台
单个 Harness 容器只是“引擎”,要变成可用的“汽车”,必须配上“油箱”(模型服务)、“方向盘”(Web UI)、“仪表盘”(日志监控)。下面是一份经过某实验室 30 台学生终端压测验证的docker-compose.yml:
version: '3.8' services: # 模型服务:使用 vLLM,加载 DeepSeek-V2-Chat-16B(量化版) vllm: image: vllm/vllm-openai:0.6.3.post1 command: > --model deepseek-ai/DeepSeek-V2-Chat --tensor-parallel-size 2 --dtype half --quantization awq --max-model-len 8192 --enable-prefix-caching --gpu-memory-utilization 0.9 ports: - "8080:8000" volumes: - ./models:/root/.cache/huggingface deploy: resources: reservations: devices: - driver: nvidia count: 2 capabilities: [gpu] # Agent 运行时:DeepSeek-Harness harness: image: harness:0.3.2-v1 ports: - "8000:8000" environment: # 指向 vLLM 服务,使用 docker-compose 内部 DNS 名称 MODEL_ENDPOINT: "http://vllm:8000/v1/chat/completions" # 工具目录挂载,便于热更新 TOOLS_DIR: "/app/tools" volumes: - ./tools:/app/tools:ro - ./config:/app/config:ro depends_on: vllm: condition: service_healthy # Web UI:一个极简 Flask 应用,仅 127 行代码 webui: build: ./webui ports: - "8081:80" environment: HARNESS_URL: "http://harness:8000" depends_on: harness: condition: service_healthy # 日志聚合:用 Loki + Promtail 收集所有容器日志 loki: image: grafana/loki:2.9.4 command: -config.file=/etc/loki/local-config.yaml volumes: - ./loki-config.yaml:/etc/loki/local-config.yaml ports: - "3100:3100" promtail: image: grafana/promtail:2.9.4 volumes: - /var/log:/var/log - ./promtail-config.yaml:/etc/promtail/config.yml depends_on: - loki4.1 服务间通信的底层机制
很多人疑惑:“为什么harness容器里写http://vllm:8000就能访问到vllm容器?” 这背后是 Docker 的user-defined bridge network在起作用。
当你运行docker-compose up,Docker 会自动创建一个名为yourproject_default的网络,并为每个 service 分配一个 DNS 名称(即 service 名)。harness容器内的/etc/hosts会被自动注入:
172.20.0.3 vllm 172.20.0.4 harness 172.20.0.5 webui所以http://vllm:8000实际解析为172.20.0.3:8000,流量不经过宿主机网卡,直接在 Docker 内部网络转发,延迟低于 0.2ms。
注意:不要用
localhost或127.0.0.1。在容器内,localhost指向容器自身,不是宿主机。必须用 service name。
4.2 工具热更新的实现原理
harness服务的volumes配置中,./tools:/app/tools:ro是关键。这意味着:
- 你在宿主机
./tools/db_query.py里修改代码; - 保存后,容器内
/app/tools/db_query.py文件内容实时更新(Linux 的 overlayfs 支持); - 下次 Harness 收到请求时,会重新 import 该模块,加载新逻辑。
但要注意:函数对象不会自动重载。如果db_query.py里有个全局变量CACHE = {},修改代码后CACHE依然存在。解决方案是:Harness 提供了--reload-tools参数,启动时加上它,就会在每次请求前检查.py文件 mtime,有变化则重新 import。
我们在某公司内部部署时,就利用这个特性实现了“运营人员无需重启服务,直接编辑 YAML 就能增删工具”的能力。
4.3 故障排查链路:当 Agent 返回空结果时怎么办?
这是最常遇到的问题。完整的排查链路如下(按顺序执行):
确认 Harness 服务健康:
curl http://localhost:8000/health # 正常返回 {"status":"healthy","tools_loaded":3,"model_connected":true}确认模型服务可达:
curl -X POST http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"deepseek-ai/DeepSeek-V2-Chat","messages":[{"role":"user","content":"你好"}]}' # 必须看到非空的 "choices[0].message.content"确认工具被正确加载:
curl http://localhost:8000/v1/tools # 返回所有已注册工具的 YAML 描述手动触发一次 Agent 运行,观察详细日志:
curl -X POST http://localhost:8000/v1/agents/run \ -H "Content-Type: application/json" \ -d '{ "query": "帮我查人工智能导论的上课时间", "tools": ["query_course_db"], "model_endpoint": "http://vllm:8000/v1/chat/completions" }'如果返回
{"error":"Tool not found"},说明query_course_db没被加载——检查tools/tools.yaml是否有拼写错误,或tools/目录权限是否为ro(只读模式下无法动态 import)。查看容器日志定位具体错误:
docker logs -f harness # 关键线索通常在 "Step 1: Calling tool query_course_db with args..." 之后 # 如果看到 "ModuleNotFoundError: No module named 'sqlite3'",说明工具依赖缺失,需在 Dockerfile 中补装
这套链路我们整理成一张 A4 纸大小的排查图,贴在实验室墙上,学生遇到问题先按图索骥,80% 的问题能在 5 分钟内自行解决。
5. 安全加固与生产就绪:让局域网平台真正“可交付”
在实验室或小团队内部跑通 demo 很容易,但要让这个平台成为“可交付资产”,必须解决三个现实问题:认证授权、资源隔离、审计追踪。下面这些措施,全部来自某公司内部 AI 平台上线前的安全评审清单。
5.1 最小权限原则:容器内进程不以 root 运行
前面 Dockerfile 中的USER 1001是基础,但还不够。还需在docker-compose.yml中显式声明:
harness: # ... 其他配置 user: "1001:1001" # 禁止容器获取宿主机的额外能力 cap_drop: - ALL # 限制内存和 CPU,防止单个容器吃光资源 mem_limit: 4g cpus: 2.0实测效果:当某个学生误写了一个无限循环的工具函数(比如while True: time.sleep(1)),容器内存被限制在 4GB,CPU 占用 capped 在 200%,不会影响vllm或webui服务。
5.2 网络层隔离:用 Docker Network 控制服务可见性
默认的docker-compose网络是互通的,但生产环境应遵循“最小暴露面”原则。修改docker-compose.yml:
networks: # 仅用于内部服务通信的私有网络 internal: driver: bridge # 仅用于对外提供 Web UI 的网络 external: driver: bridge services: vllm: networks: - internal # 不暴露到 external 网络,防止学生直接调用模型 API # ports: [] # 注释掉所有 ports harness: networks: - internal - external # 只暴露 8000 端口给 external,且仅限于 /v1/agents/run ports: - "8000:8000" webui: networks: - external # Web UI 是唯一对外入口,所有请求经它转发这样,学生只能通过http://server-ip:8081访问 Web 页面,无法直连http://server-ip:8000或http://server-ip:8080。安全边界清晰。
5.3 审计日志:记录每一次 Agent 调用的完整上下文
Harness 默认日志只输出到 stdout,不利于审计。我们在webui服务中增加了日志落盘逻辑:
# webui/app.py 片段 from loguru import logger import json @app.post("/api/run") async def run_agent(query: str): # 记录原始请求 logger.info(f"AGENT_CALL_START | user_ip={request.client.host} | query='{query}'") # 调用 harness response = requests.post( f"{HARNESS_URL}/v1/agents/run", json={"query": query, "tools": get_enabled_tools()} ) # 记录完整响应(脱敏后) result = response.json() safe_result = { "final_answer": result.get("final_answer", "")[:200] + "...", # 截断长文本 "step_count": len(result.get("steps", [])), "tool_calls": [s["tool_name"] for s in result.get("steps", [])] } logger.info(f"AGENT_CALL_END | status={response.status_code} | {json.dumps(safe_result)}") return result日志格式统一为AGENT_CALL_*,便于用grep或 Loki 查询。例如,查某学生 IP 的所有调用:
grep "192.168.1.105" /var/log/webui.log | head -205.4 模型权重的离线管理:彻底摆脱网络依赖
虽然 Harness 本身不联网,但如果vllm启动时需要从 Hugging Face 下载模型,就会失败。解决方案是:预下载 + 挂载。
在宿主机上执行:
# 创建模型缓存目录 mkdir -p ./models # 使用 huggingface-hub 工具离线下载(需提前在有网环境运行) huggingface-cli download deepseek-ai/DeepSeek-V2-Chat \ --local-dir ./models/deepseek-ai/DeepSeek-V2-Chat \ --revision main然后在docker-compose.yml中,vllm服务的volumes挂载该目录:
vllm: volumes: - ./models:/root/.cache/huggingface:rovLLM 启动时会优先检查本地路径,命中即跳过网络请求。我们测试过:在完全断网的环境中,vllm从启动到 ready 仅需 18 秒,比联网下载快 7 倍。
6. 从“能跑”到“好用”:五个真实场景下的定制化技巧
上面讲的是通用部署,但每个团队的需求都不同。以下是我在不同客户现场总结的、开箱即用的定制技巧,全部经过实测。
6.1 技巧一:让 Agent “记住”学生姓名(无数据库方案)
需求:学生第一次问“我是谁”,Agent 应回答“你是张三”;第二次问,仍回答“你是张三”。但不想引入 Redis 或 SQLite。
解法:利用 Harness 的session_id字段 + 内存字典。
在tools/session_tool.py中:
from harness.tool import tool # 全局内存存储,生产环境应换为 Redis,但局域网小规模够用 SESSION_STORE = {} @tool def get_student_name(session_id: str) -> str: """Return student name from session store""" return SESSION_STORE.get(session_id, "未知学生") @tool def set_student_name(session_id: str, name: str) -> str: """Store student name in session store""" SESSION_STORE[session_id] = name return f"已记录:{name}"在 Web UI 的每次请求中,传入session_id(比如用学生学号):
{ "query": "我是谁", "session_id": "20230001", "tools": ["get_student_name"] }注意:内存字典在容器重启后丢失。如需持久化,将
SESSION_STORE替换为shelve.open("/data/sessions.db"),并挂载宿主机目录。
6.2 技巧二:用 Shell 工具执行“不可信”操作
需求:允许学生运行df -h查磁盘,但禁止执行rm -rf /。
解法:Harness 的工具机制天然支持白名单。写一个safe_shell.py:
import subprocess from harness.tool import tool ALLOWED_COMMANDS = { "df": ["-h", "-hT"], "ls": ["-la", "-l", ""], "ping": ["-c", "3"] } @tool def run_safe_command(command: str, args: list[str]) -> str: """Run only pre-approved shell commands""" if command not in ALLOWED_COMMANDS: return f"命令 '{command}' 不在白名单中" if not all(arg in ALLOWED_COMMANDS[command] for arg in args): return f"参数 {args} 不被允许用于 '{command}'" try: result = subprocess.run([command] + args, capture_output=True, text=True, timeout=10) return result.stdout[:2000] # 限制输出长度 except subprocess.TimeoutExpired: return "命令执行超时" except Exception as e: return f"执行失败:{str(e)}"在tools/tools.yaml中声明:
run_safe_command: description: "Run approved shell commands" parameters: command: type: string description: "Command name, e.g., 'df', 'ls'" args: type: array items: type: string description: "Arguments, e.g., ['-h']"6.3 技巧三:为不同学生分配不同模型
需求:老师用 16B 模型,学生用 7B 模型,避免资源争抢。
解法:Harness 支持 per-request 指定model_endpoint。在 Web UI 中,根据登录用户角色动态切换:
# webui/app.py def get_model_endpoint(user_role: str) -> str: if user_role == "teacher": return "http://vllm-16b:8000/v1/chat/completions" else: return "http://vllm-7b:8000/v1/chat/completions" # 在调用 harness 时传入 requests.post( f"{HARNESS_URL}/v1/agents/run", json={ "query": query, "model_endpoint": get_model_endpoint(role), "tools": tools } )对应地,在docker-compose.yml中启动两个 vLLM 实例,分别加载不同模型。
6.4 技巧四:用 YAML 配置替代硬编码工具链
需求:不改 Python 代码,就能启用/禁用某个工具。
解法:Harness 支持--tools-config参数,指向一个 YAML 文件。创建config/tools-config.yaml:
enabled_tools: - query_course_db - run_safe_command disabled_tools: - set_student_name # 暂时禁用启动 Harness 时:
CMD ["uvicorn", "harness.main:app", "--host", "0.0.0.0:8000", "--tools-config", "/app/config/tools-config.yaml"]Harness 会自动过滤tools/目录中未在enabled_tools列表里的函数。