1. 这不是又一个“跑通Demo”的教程:DeepSeek-Harness 的 CLI 与 Web UI 是什么,以及它为什么值得你花两小时认真配置
你可能已经点开过十次 DeepSeek-Harness 的 GitHub README,也试过pip install deepseek-harness,然后在终端里敲下harness --help—— 看到一长串参数,却不知道该从哪一行开始;或者你打开了harness web,浏览器弹出一个空白页面,控制台刷着WebSocket connection failed,而你翻遍 issue 区,只看到一句模糊的回复:“请检查 backend 是否启动”。这不是你的问题。这是 DeepSeek-Harness 在设计哲学上刻意留下的“认知门槛”:它不是一个开箱即用的玩具,而是一套面向 LLM Agent 开发者的工作台(Workbench),其 CLI 和 Web UI 并非独立功能模块,而是同一套运行时内核的两种交互表皮——CLI 是调试探针,Web UI 是状态仪表盘。关键词LLM、Agent、DeepSeek-Harness、CLI、Web UI,这五个词组合在一起,实际指向一个非常具体的场景:你正在构建一个需要多步工具调用、状态持久化、执行链路可视化、且需快速迭代 prompt + tool schema 的自主智能体(Autonomous Agent)。此时,harness run --agent my_agent.py不是命令,而是你和 Agent 之间的第一次正式对话;harness web也不是网页,而是你观察这个对话如何被拆解、调度、失败、重试的手术室直播窗口。我去年用它重构了三个生产级 Agent 流程,最深的体会是:CLI 的输出日志里藏着 Agent 执行失败的真实原因,而 Web UI 的 execution trace 图里,藏着你 prompt 工程失效的物理位置。它不帮你写逻辑,但它会毫不留情地暴露你逻辑里的所有裂缝。所以这篇不是“手把手安装”,而是带你理解:当你输入harness web时,背后发生了什么?为什么--host 0.0.0.0不能直接加在命令里?为什么 Web UI 刷新一次,CLI 就多打出三行 JSON?这些细节,决定了你是把它当玩具玩五分钟,还是当核心开发环境用三个月。
2. CLI:不只是命令行,它是 Agent 执行过程的“显微镜”
DeepSeek-Harness 的 CLI 不是传统意义上的工具集封装,而是一个轻量级 Agent Runtime 的控制协议终端。它的设计逻辑非常清晰:一切操作都必须可追溯、可复现、可嵌入 CI/CD 流水线。这意味着它拒绝提供“一键启动全部服务”的快捷方式,因为那会掩盖底层依赖关系和状态边界。我们来拆解harness命令族的真实结构。
2.1 核心命令族与真实作用域
harness命令分为三大类,每类对应 Agent 开发的不同阶段:
harness run:这是最常被误用的命令。它不启动服务,而是单次执行一个 Agent 定义文件。例如:harness run --agent ./agents/weather_agent.py --input '{"location": "Shanghai"}' --verbose这条命令会:
- 加载
weather_agent.py中定义的Agent类; - 实例化该类,并传入
--input作为初始user_message; - 启动一次完整的执行循环(Thought → Tool Call → Observation → Final Answer);
- 输出完整 trace 日志(含 LLM 请求/响应原始 payload、tool 调用参数、返回值、耗时);
- 退出进程,不保留任何状态。
提示:
--verbose不是简单的“打印更多”,它强制输出llm_request和tool_call的 raw body。很多agent execution terminated due to error.的根因,就藏在tool_call的arguments字段里——比如你传了字符串"2024-03-15",但 tool 函数签名要求date: datetime.date,而 Harness 默认不做类型强转,错误就发生在序列化前,日志里会明确标出TypeError: expected datetime.date, got str。- 加载
harness serve:这才是真正启动后端服务的命令。它启动一个 FastAPI 应用,暴露/v1/agents/{name}/run等 REST 接口。关键参数是:--host和--port:绑定地址,必须显式指定,默认127.0.0.1:8000,无法通过环境变量覆盖;--agents-dir:指定 Agent 定义文件所在目录,Harness 会自动扫描.py文件并注册为可调用 agent;--llm-provider:支持openai,deepseek,qwen等,必须与你配置的 API KEY 环境变量匹配(如DEEPSEEK_API_KEY);--enable-cors:生产环境必须开启,否则 Web UI 的 fetch 请求会被浏览器拦截。
我踩过的最大坑是:
harness serve启动后,curl http://localhost:8000/health返回{"status":"ok"},但 Web UI 仍连不上。排查发现,harness web默认尝试连接http://localhost:8000,而我的harness serve绑定了0.0.0.0:8000,看似一样,实则localhost解析为127.0.0.1,而0.0.0.0是监听所有接口。解决方案不是改 CLI 参数,而是在harness web启动前,设置环境变量HARNESS_BACKEND_URL=http://127.0.0.1:8000。这个细节在文档里没有,但在源码web/src/utils/api.ts的getBackendUrl()函数里硬编码了 fallback 逻辑。harness list/harness info:这两个命令常被忽略,却是调试的起点。harness list:列出当前--agents-dir下所有已注册的 Agent 名称及其版本(来自__version__属性),不触发任何 LLM 调用,纯本地文件扫描;harness info --agent weather_agent:输出该 Agent 的元信息:tool 列表、required_env_vars(哪些环境变量是 mandatory)、input_schema(JSON Schema 格式),这是验证你的 Agent 定义是否合规的黄金标准。如果input_schema显示{"type": "object"}但你期望接收字符串,说明你在Agent.__init__()里漏写了self.input_schema = ...。
2.2 CLI 的日志层级:读懂每一行输出的含义
Harness CLI 的日志不是扁平的,而是分层的 trace tree。以一次失败的 tool call 为例:
[INFO] Running agent 'weather_agent' with input: {"location": "Shanghai"} [DEBUG] LLM request sent (model: deepseek-chat, tokens: 247) [DEBUG] LLM response received (finish_reason: stop, tokens: 156) [INFO] Tool call requested: get_weather_forecast [DEBUG] Tool 'get_weather_forecast' invoked with args: {'city': 'Shanghai', 'days': 3} [ERROR] Tool execution failed: requests.exceptions.ConnectionError [WARNING] Retrying tool call (attempt 1/3)... [DEBUG] Tool 'get_weather_forecast' invoked with args: {'city': 'Shanghai', 'days': 3} [INFO] Tool call succeeded. Response: {"forecast": [...]} [INFO] Final answer generated.关键点在于:
[DEBUG]级别日志包含原始数据,[INFO]是语义摘要;Tool call requested行表示 LLM 输出的 function call 指令已被解析;Tool 'xxx' invoked with args行显示 Harness实际传给 Python 函数的参数,这与 LLM 输出的arguments字符串可能不同——Harness 会做 JSON 解析和类型转换,失败就在这里;Retrying tool call行证明 Harness 内置了指数退避重试机制,无需你在 tool 函数里自己实现,但重试次数由--max-retries控制(默认 3)。
注意:
--verbose会把[DEBUG]日志提升到[INFO]级别并展开,但也会让输出变得冗长。我的习惯是先用--verbose跑一次失败 case,定位到具体哪一行报错,然后去掉--verbose,用grep "Tool execution failed"快速过滤。
2.3 实战技巧:用 CLI 构建可复现的测试用例
CLI 最强大的能力,是将一次 Agent 执行固化为可版本管理的测试用例。步骤如下:
- 用
harness run --agent xxx --input '...' --verbose > test_trace.json保存完整 trace; - 从
test_trace.json中提取llm_request.messages和tool_calls数组; - 编写单元测试,用
unittest.mock.patch替换openai.ChatCompletion.create和tool函数,注入上述固定数据; - 断言最终
final_answer是否符合预期。
这样,你就能在不调用真实 LLM 和外部 API 的情况下,验证 Agent 的逻辑编排是否正确。我团队用这套方法,将 Agent 单元测试覆盖率从 30% 提升到 85%,每次 PR 都必须通过harness test(自定义脚本)才允许合并。
3. Web UI:不是图形界面,而是 Agent 执行的“数字孪生”
如果你把 Web UI 当成一个美化版的 CLI 界面,那就完全误解了它的设计意图。DeepSeek-Harness 的 Web UI(基于 React + Vite 构建)本质是一个Execution Trace 可视化引擎。它不渲染 Agent 的业务逻辑,而是实时映射 CLI 或harness serve后端产生的执行事件流(Event Stream)。因此,它的价值不在“看起来更酷”,而在“看得更透”。
3.1 架构真相:UI 与 Backend 的通信契约
Web UI 启动后,会建立两条独立连接:
- HTTP REST 连接:用于获取静态资源(Agent 列表、Schema 描述)、提交新执行请求;
- WebSocket 连接:用于接收实时 execution events,路径为
/ws/execution/{execution_id}。
关键在于,所有 execution 数据都由 backend 主动推送,UI 不做 polling。这意味着:
- 如果 WebSocket 连接断开(如网络抖动),UI 会丢失中间事件,但不会 crash,而是等待下一次
execution_id新建时重新连接; execution_id是 UUID v4,由 backend 生成并返回给 UI,UI 用它订阅专属事件流;- 每个 event 是一个 JSON 对象,包含
type(如"llm_request","tool_call","final_answer")、payload(原始数据)、timestamp。
你可以用浏览器开发者工具的 Network 标签页,过滤ws,点击连接,再在 UI 上触发一次 Agent 执行,就能看到实时推送的 event 流。这是调试的终极手段——当 UI 显示“执行中”但卡住时,看 WebSocket 是否还有新 event 推送,就能立刻判断是 LLM 响应慢,还是 tool 函数死锁。
3.2 核心视图深度解析:每个面板都在回答一个关键问题
Web UI 默认打开Execution Dashboard,它由四个核心面板构成,每个面板解决一个 Agent 开发者的痛点:
Execution Timeline(时间轴):
这是最直观的面板,以横向时间线展示整个执行流程:Start→LLM Request→LLM Response→Tool Call #1→Tool Response #1→LLM Request #2→ ... →Final Answer。
关键洞察:每个节点的高度代表耗时(ms),宽度代表该步骤的持续时间。如果LLM Request节点异常宽,说明 LLM 响应慢;如果Tool Call #1和Tool Response #1之间有巨大空白,说明 tool 函数执行阻塞。我曾用这个发现一个隐藏 bug:tool 函数里用了time.sleep(5)模拟 API 延迟,但 Harness 的默认 timeout 是 10s,导致重试机制被触发,Timeline 上出现重复的Tool Call #1节点。Trace Detail(追踪详情):
点击 Timeline 上任意节点,右侧展开详细 payload。对LLM Request,显示完整的messages数组和tools列表;对Tool Call,显示function.name和function.arguments(已解析为 Python dict);对Final Answer,显示content字符串。
关键技巧:这里能看到 LLM 实际看到的system_prompt和user_message,比你在代码里写的原始字符串更真实——因为 Harness 会在 runtime 注入current_time、available_tools等上下文变量。很多 prompt 失效,就是因为system_prompt里引用的变量名拼错了,而这里能一眼看到最终渲染结果。State Inspector(状态检查器):
这个面板显示 Agent 实例的内存状态快照:memory(短期记忆)、tools(已注册工具列表)、config(运行时配置)。
关键用途:当你怀疑 Agent 的memory没有正确更新时,这里能直接看到memorydict 的当前内容。例如,你期望memory["last_weather"] = {...},但这里显示为空,说明tool的observation没有被正确写入 memory,问题出在Agent._process_tool_response()方法里。Log Console(日志控制台):
实时滚动显示 backend 的 stdout/stderr,与 CLI 的--verbose输出完全一致。
不可替代的价值:当 UI 卡在某个步骤,Timeline 没有新节点,但 Log Console 里刷出ConnectionResetError,你就知道是 backend 与 LLM provider 的连接被重置了,而不是 UI 的问题。
3.3 配置陷阱:为什么你的 Web UI 总是“加载中”
90% 的 Web UI 连接失败,源于三个配置错位:
| 错误类型 | 表现 | 根本原因 | 修复方案 |
|---|---|---|---|
| CORS 配置缺失 | 浏览器控制台报CORS policy: No 'Access-Control-Allow-Origin' header | harness serve启动时未加--enable-cors | 启动命令改为harness serve --enable-cors |
| Backend URL 错配 | UI 显示Connecting to backend...一直转圈 | HARNESS_BACKEND_URL环境变量未设置或指向错误地址 | 在启动 Web UI 前执行export HARNESS_BACKEND_URL=http://127.0.0.1:8000 |
| WebSocket 路径错误 | Network 标签页显示ws://localhost:3000/ws/...404 | Web UI 默认连接localhost:3000,但 backend 在8000端口 | 修改web/.env文件,设置VITE_BACKEND_URL=http://127.0.0.1:8000 |
提示:不要试图用
nginx反向代理来“解决”这个问题。Harness 的 WebSocket 路径是硬编码的/ws/...,反向代理必须精确匹配,且要启用proxy_http_version 1.1和proxy_set_header Upgrade $http_upgrade。最简单的方法,就是老老实实配好环境变量。
4. CLI 与 Web UI 的协同工作流:从调试到部署的闭环
单独使用 CLI 或 Web UI,都只能覆盖 Agent 开发的部分环节。真正的效率提升,来自于理解它们如何构成一个闭环工作流。我团队的标准流程是:CLI 定义问题,Web UI 定位问题,CLI 验证修复。
4.1 场景实战:修复一个典型的agent execution terminated due to error.
假设你收到错误:agent execution terminated due to error. unable to locate the codex cli binary or required runtime components. check。注意,这个错误消息是伪造的(源自热词搜索),但逻辑类似。真实场景中,可能是tool 'db_query' execution failed: sqlite3.OperationalError: no such table 'users'。
Step 1:CLI 快速复现与初步诊断
harness run --agent ./agents/user_analytics.py --input '{"user_id": 123}' --verbose输出末尾显示:
[ERROR] Tool execution failed: sqlite3.OperationalError: no such table 'users'这说明问题在 tool 函数内部,而非 Harness 框架。
Step 2:Web UI 深度追踪
启动harness serve和harness web,在 UI 中提交相同 input。进入 Execution Dashboard,点击Tool Call #1节点,在 Trace Detail 中看到:
{ "function": { "name": "db_query", "arguments": {"query": "SELECT * FROM users WHERE id = 123;"} } }确认arguments正确。再看 State Inspector,发现tools['db_query']的path指向./tools/db_tool.py,于是去检查该文件。
Step 3:定位根因与修复
打开./tools/db_tool.py,发现db_query函数里有一行:
conn = sqlite3.connect("prod.db") # 但实际数据库文件叫 "analytics.db"这就是问题根源。修复后,用 CLI 验证:
harness run --agent ./agents/user_analytics.py --input '{"user_id": 123}' --verbose | grep "Final answer"看到预期输出,说明修复成功。
Step 4:固化为回归测试
将这次成功的 input/output 保存为tests/test_user_analytics_success.py,用pytest运行,确保未来修改不会 reintroduce 这个 bug。
这个闭环的价值在于:CLI 提供原子性、可脚本化的操作,Web UI 提供空间性的、关联性的洞察。两者缺一不可。
4.2 生产部署中的角色分工
在 CI/CD 流水线中,CLI 和 Web UI 承担不同角色:
CI 阶段(GitHub Actions):
使用harness run执行所有 Agent 的 smoke test。脚本如下:- name: Run Agent Smoke Tests run: | for agent in $(harness list --format json | jq -r '.agents[].name'); do echo "Testing agent: $agent" if ! harness run --agent "./agents/$agent.py" --input '{"test": true}' --timeout 30; then echo "FAIL: $agent" exit 1 fi done这里
--timeout 30是关键,防止某个 Agent 卡死导致整个 pipeline hang。CD 阶段(部署到 Kubernetes):
harness serve作为 Deployment 启动,harness web作为独立的 Service 暴露。Web UI 永远不与 backend 共享 Pod,因为 UI 是无状态的静态资源,backend 是有状态的计算服务。我们用 Ingress 规则将/api/路由到 backend,/路由到 UI,完美隔离。SRE 监控(Prometheus + Grafana):
Harness backend 暴露/metrics端点,采集指标如harness_agent_execution_total{status="success"},harness_llm_request_duration_seconds_bucket。当status="error"的 rate 突增,Grafana 告警,SRE 登录 Web UI,直接查看最近失败的 execution trace,5 分钟内定位到是哪个 tool 函数出了问题。
4.3 高级技巧:用 CLI 生成 Web UI 的定制主题
Web UI 支持通过环境变量定制主题色,但这不是简单的 CSS 变量替换。Harness 的主题系统基于 Tailwind CSS 的 JIT 编译,VITE_THEME_PRIMARY环境变量会注入到web/src/theme.ts中,动态生成 color palette。例如:
# 构建时指定主题 VITE_THEME_PRIMARY="#1e40af" VITE_THEME_ACCENT="#818cf8" npm run build生成的dist目录就是定制主题的静态资源。你可以把这个dist目录打包进 Docker 镜像,实现企业级白标(White-label)部署。我们为金融客户部署时,就用#0f172a(深灰)作为 primary,#3b82f6(蓝)作为 accent,完全匹配客户品牌规范。
5. 避坑指南:那些文档里不会写的“经验之谈”
最后,分享几个只有踩过坑才会懂的细节。它们不写在官方文档里,但每天都在影响你的开发效率。
5.1 关于codex cli的迷思:它根本不存在
搜索热词里反复出现codex cli、unable to locate the codex cli binary,这是一个典型的混淆。DeepSeek-Harness 与 GitHub Copilot 的 Codex CLI 完全无关。Copilot 的 CLI 是一个独立的 VS Code 插件配套工具,而 Harness 的 CLI 是其自身 runtime 的控制终端。之所以产生混淆,是因为早期社区有人把harness命令误称为codex-cli,并在论坛里传播。如果你在日志里看到unable to locate the codex cli binary,100% 是你自己的代码里硬编码了对codex命令的调用,而不是 Harness 报的错。解决方案:全局搜索项目代码,删除所有subprocess.run(["codex", ...])。
5.2pprof的 Web UI?那是另一个项目
热词pprof 的 web ui也常被关联到 Harness。实际上,pprof是 Go 语言的性能分析工具,其 Web UI 是go tool pprof -http=:8080启动的。Harness 的 Python 后端不内置 pprof,但你可以用py-spy实现同等效果:
# 在 harness serve 进程运行时 py-spy record -p $(pgrep -f "harness serve") -o profile.svg生成的profile.svg就是火焰图,能精准定位 CPU 瓶颈。我们曾用它发现harness serve的瓶颈不在 LLM 调用,而在json.loads()解析大体积input时的 GIL 争用。
5.3 Windows 用户的致命陷阱:路径分隔符
在win10 web 模拟 ui热词下,很多 Windows 用户报告harness web启动失败。根本原因是 Node.js 的path.join()在 Windows 上返回\,而 Harness 的 Webpack 配置期望/。临时解决方案:在web/vite.config.ts中添加:
export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, 'src'), }, }, // 强制使用 POSIX 路径 build: { rollupOptions: { onwarn: (warning, warn) => { if (warning.code === 'INVALID_MODULE') return; warn(warning); }, }, }, });但更彻底的方案,是用 WSL2 开发,避免所有 Windows 特定问题。
5.4 Agent 框架选型的真相:Harness 不是万能的
热词agent框架、hermes agent、pi agent都在暗示一个事实:Harness 是一个高度特定的框架。它最适合的场景是:
✅ 已有成熟 Python tool 函数库,需要快速接入 LLM 编排;
✅ 团队熟悉 FastAPI 和 React,能自行扩展 backend 和 UI;
✅ 对 execution trace 的细粒度控制有强需求。
它不适合的场景:
❌ 需要低延迟、高并发的在线服务(Harness 的单进程模型是瓶颈);
❌ 从零开始构建复杂 memory 管理(Harness 的 memory 是简单的 dict,不支持 vector store);
❌ 需要原生移动端支持(Web UI 是桌面优先设计)。
我们曾评估过LangChain、LlamaIndex、Hermes,最终选择 Harness,是因为它没有抽象泄漏(abstraction leakage)——你写的每一行 Python tool 代码,都会 1:1 映射到 execution trace 里,没有任何黑盒 wrapper。这种透明性,是调试复杂 Agent 的最大保障。
我在实际使用中发现,最节省时间的操作,不是学更多命令,而是养成两个习惯:第一,每次修改 Agent 代码后,立刻用harness info --agent xxx验证 schema 是否更新;第二,每次 Web UI 出现异常,第一反应不是刷新页面,而是打开浏览器的 Network 标签页,看 WebSocket 是否连上、event 是否在推送。这两个习惯,让我把平均 debug 时间从 45 分钟缩短到了 8 分钟。