2026/10/17 4:21:50

Docker部署DeepSeek-Harness构建局域网AI Agent平台

Docker部署DeepSeek-Harness构建局域网AI Agent平台 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 不等于 CPUHarness 也不等于 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推荐吞吐高、支持 streamingOllama 运行的 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 searchHarness 启动时会自动扫描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 APIPOST /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 11glibc 版本稳定兼容绝大多数科学计算包 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 --chown1001:1001 . . # 复制自定义工具目录保持权限一致 COPY --chown1001:1001 tools/ tools/ # 暴露 Harness 默认端口8000便于 docker run -p 映射 EXPOSE 8000 # 健康检查每 30 秒用 curl 检查 /health 端点超时 3 秒失败 3 次即标记为 unhealthy HEALTHCHECK --interval30s --timeout3s --start-period5s --retries3 \ 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而不是alpineAlpine 使用 musl libc而很多 Python 包尤其是带 C 扩展的 numpy、pandas、protobuf在 musl 下编译异常或运行时崩溃。我们曾在一个树莓派项目中用 alpine结果harness run命令执行到第 7 次就 segmentation fault。换成 bullseye 后连续运行 72 小时无异常。为什么apt-get install里包含libpq-dev和gcc即使你当前不用 PostgreSQLpsycopg2是 Python 生态中最稳定的数据库驱动很多自定义工具会依赖它。而gcc是编译numpy的必需项——slim 镜像默认不带编译器pip install numpy会回退到纯 Python 版本性能下降 5 倍以上。实测显示加了gcc后import numpy速度从 1.2 秒降到 0.15 秒。为什么HEALTHCHECK用curl而不是ncncnetcat只能检测端口是否开放但无法判断应用是否 ready。Harness 启动后可能还要加载工具、连接模型服务此时端口已 open但/v1/agents/run仍会返回 503。而/health端点是 Harness 内置的它会检查工具加载状态和模型 endpoint 连通性。用curl能真正反映服务健康度。为什么CMD里--workers 2Harness 是 CPU-bound 任务解析 JSON、调用工具、拼接字符串不是 I/O-bound。单 worker 在高并发下容易阻塞。测试数据显示2 workers 时QPS 从 8 提升到 224 workers 时 QPS 反而降到 19GIL 争用加剧。所以 2 是性价比最优解。3.2 requirements.txt 的精简之道requirements.txt是镜像大小和启动速度的关键。以下是经过裁剪的最小可行集合基于 harness v0.3.2# DeepSeek-Harness 核心依赖 fastapi0.115.0 uvicorn[standard]0.32.0 pydantic2.9.2 jinja23.1.4 # 工具开发常用依赖按需添加此处为通用场景 requests2.32.3 python-dotenv1.0.1 # 日志和监控可选但强烈建议 loguru0.7.2注意两点绝不写deepseek-harness*或deepseek-harness0.3.0。必须锁定具体版本号如githttps://github.com/deepseek-ai/harness.gitv0.3.2#eggdeepseek-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.ymlversion: 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 upDocker 会自动创建一个名为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)容器内存被限制在 4GBCPU 占用 capped 在 200%不会影响vllm或webui服务。5.2 网络层隔离用 Docker Network 控制服务可见性默认的docker-compose网络是互通的但生产环境应遵循“最小暴露面”原则。修改docker-compose.ymlnetworks: # 仅用于内部服务通信的私有网络 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(fAGENT_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(fAGENT_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.pyimport 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_outputTrue, textTrue, timeout10) 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.yamlenabled_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列表里的函数。6.5