看不见的 Agent 很难被信任
一个任务运行了十分钟,界面上只有“思考中”。用户不知道它是在搜索资料、修改文件、等待外部接口,还是已经陷入循环。
即使最后成功,开发者也很难回答:时间花在哪里?哪个工具最常失败?哪条策略阻止了操作?为什么同类任务昨天能完成,今天却超时?
这就是 Harness 第七个核心部分要解决的问题:可观察性与用户控制。
可观察性让系统内部发生的事情能够被理解,用户控制则让人可以在正确的时机暂停、取消、批准或纠正。两者合在一起,Agent 才不是一个只能等待结果的黑盒。
本文是《把 Harness 讲清楚:Agent 背后的运行系统》八个核心部分的第七篇。
日志、指标和追踪有什么区别
三者都重要,但解决的问题不同。
日志
记录离散事件,适合回答“具体发生了什么”。例如工具参数校验失败、审批被拒绝、模型请求返回限流。
指标
记录可聚合数字,适合回答“整体表现如何”。例如任务成功率、P95 延迟、平均工具调用数和每天 token 成本。
追踪
把一次任务中的多个阶段串成因果链,适合回答“时间花在哪里、上下游怎样关联”。
一个 Trace 可以包含:
Task Trace:修复登录 Bug,18.4 秒
├─ Context Retrieval,1.2 秒
├─ Model Step 1,3.8 秒
├─ Tool:search_files,0.3 秒
├─ Tool:read_file,0.1 秒
├─ Model Step 2,4.6 秒
├─ Tool:edit_file,0.2 秒
├─ Tool:run_tests,6.9 秒
└─ Final Evaluation,1.3 秒
只有日志,能看到事件却难以理解整体时序;只有指标,能看到异常趋势却不知道具体样本;追踪把两者连接起来。
应该观测哪些阶段
Harness 至少应为这些对象建立稳定标识:
task_id:一个用户目标;turn_id:一次被输入唤醒的运行;step_id:一次模型请求及其工具执行;tool_call_id:一次具体工具调用;approval_id:一次用户决策;trace_id:贯穿相关服务的追踪标识。
每个阶段记录开始、结束、耗时、状态和错误分类。对模型调用,还可以记录模型名称、输入输出 token、缓存命中和停止原因;对工具调用,记录工具名、风险等级、执行环境和结果大小。
敏感参数和模型正文不应默认完整进入遥测系统。可观察性不能以泄露隐私为代价。
一次任务的事件流
task/accepted
↓
turn/started
↓
context/assembled
↓
model/requested → model/completed
↓
tool/authorized
├─ approval/requested → approval/decided
↓
tool/started → tool/completed
↓
evaluation/completed
↓
turn/completed
↓
task/completed
如果任务失败,应该能从事件中看到它停在哪一层,而不是所有失败都变成一个 task_failed。
面向开发者和面向用户的信息不同
内部观测可以非常细,但不应把所有调试信息倾倒给用户。
开发者关心:
- 请求与响应时间;
- 重试、限流和错误码;
- 工具参数的安全摘要;
- 状态 revision 和事件序号;
- token、费用和缓存;
- 调度队列与并发情况。
用户更关心:
- 当前目标和阶段;
- 已完成的重要动作;
- 正在等待什么;
- 是否需要自己做决定;
- 是否可以停止;
- 最终结果与证据。
好的 UI 会把内部事件翻译成简洁进度,例如“已定位到 3 个相关文件,正在运行回归测试”,而不是持续显示模型的私有推理文本。
为什么不应该展示完整思维过程
用户需要的是行动依据和可验证事实,不是未经整理的内部推理流。
完整思维过程可能包含错误猜测、敏感上下文和大量噪声,也会让界面难以阅读。更合适的做法是展示:
- 当前计划;
- 关键决策及简短理由;
- 工具调用与结果摘要;
- 风险和审批信息;
- 可以复核的证据。
这既保留透明度,又不会把“模型想到的每一个词”误当成稳定解释。
用户应该能怎样控制任务
取消
立即停止启动新工作,并向正在执行的操作传播取消。已经开始的副作用要等待结算或标记为结果未知。
暂停与恢复
在安全边界保存状态,稍后继续。恢复时重新检查权限、凭据和外部环境。
审批与拒绝
对具体高风险动作做一次性决策,并保留审计记录。
补充与纠正
用户可以添加新信息、缩小范围或指出方向错误。Harness 要明确这条消息进入当前步骤、下一步骤还是新一轮。
接管
Agent 无法可靠继续时,用户可以手动完成某一步,再把新状态交回系统。
这些操作都不应只是前端按钮。它们需要进入持久状态,并由 Agent Loop 和工具调度器真正执行。
进度如何做到既真实又简洁
不要让模型自由编造“完成了 70%”。更可靠的进度来自真实阶段和证据:
def user_progress(task_state):
return {
"phase": task_state.current_phase,
"completed": [
step.title
for step in task_state.steps
if step.has_verified_result
],
"running": task_state.running_action.safe_summary,
"blocked_by": task_state.pending_approval,
"can_cancel": task_state.phase == "running",
}
对于开放式探索,不必给出虚假的百分比。展示“已检查什么、正在做什么、还需要什么”通常更诚实。
指标应该围绕任务结果
模型请求次数和 token 是基础指标,却不能代表产品效果。
建议分四层观察:
| 层级 | 代表指标 |
|---|---|
| 结果 | 完成率、验证通过率、人工返工率 |
| 行为 | 工具成功率、重复调用率、越权阻止率 |
| 体验 | 总延迟、首次有效进度时间、接管率 |
| 成本 | token、外部 API、计算和存储成本 |
“策略阻止率高”既可能说明攻击很多,也可能说明规则误伤严重。指标必须结合具体 Trace 和用户反馈解释。
观测代码应该怎样接入
业务代码不应该到处手写互不一致的日志字符串。可以通过统一事件和 span 包装:
async def run_tool(call, trace):
with trace.span("tool.call") as span:
span.set("tool.name", call.name)
span.set("tool.risk", call.risk)
started_at = clock.now()
try:
result = await tool_gateway.execute(call)
span.set("result.status", result.status)
metrics.tool_latency.observe(clock.now() - started_at)
return result
except Exception as error:
span.record_error(classify(error))
raise
参数只记录经过工具定义生成的安全摘要,禁止直接把完整命令、邮件正文或访问令牌塞进 span。
告警要指向可行动的问题
“Agent 错误数增加”过于宽泛。更有效的告警包括:
- 某个模型提供方 P95 延迟连续 15 分钟超标;
- 发布工具的结果未知比例高于阈值;
- 同一任务重复工具调用次数异常;
- 策略拒绝突然集中在某个新版本;
- 用户取消后仍有新工具调用启动;
- 完成率下降,但模型请求成功率正常。
最后一种情况尤其重要:基础设施都成功,并不代表 Agent 完成了用户目标。
常见的失败方式
只有一堆字符串日志
没有 task、step 和 call 关联标识,跨服务后无法重建完整过程。
为了调试记录全部内容
模型输入、工具参数和结果可能包含秘密与个人数据。遥测需要最小化、脱敏和访问控制。
UI 只显示“思考中”
用户无法判断是否有进展,也不知道何时该取消或介入。
只看 token,不看完成率
降低 token 可能只是让 Agent 更早放弃。成本指标必须和验证结果一起看。
取消按钮只改变前端状态
界面显示已停止,但后台仍在调用工具。控制信号必须贯穿 Loop、调度器和具体运行时。
可观察性与用户控制检查清单
- Task、Turn、Step、Tool Call 是否有稳定关联 ID?
- 日志、指标和 Trace 是否各司其职?
- 是否记录阶段状态、耗时、错误类型和资源成本?
- 遥测是否默认隐藏凭据、正文和敏感参数?
- 用户是否能看到真实阶段、重要动作和阻塞原因?
- 是否用计划、证据和简短理由替代完整思维倾倒?
- 取消、暂停、审批和纠正是否真正进入运行时?
- 告警是否能定位具体模块和可行动原因?
- 运营指标是否同时覆盖结果、行为、体验和成本?
- 能否从一次失败任务快速还原完整链路?
最后理解可观察性与用户控制
可观察性不是上线后才补的运维功能,用户控制也不是给界面加几个按钮。它们必须建立在 Harness 的事件、状态和取消语义之上。
当系统既能说明自己正在做什么,又能在用户介入时真正改变执行,Agent 才会从黑盒自动化变成可协作的工具。
一句话总结:让过程看得见,让风险停得下,让用户始终保有最终控制权。