## 看不见的 Agent 很难被信任

一个任务运行了十分钟，界面上只有“思考中”。用户不知道它是在搜索资料、修改文件、等待外部接口，还是已经陷入循环。

即使最后成功，开发者也很难回答：时间花在哪里？哪个工具最常失败？哪条策略阻止了操作？为什么同类任务昨天能完成，今天却超时？

这就是 Harness 第七个核心部分要解决的问题：**可观察性与用户控制**。

可观察性让系统内部发生的事情能够被理解，用户控制则让人可以在正确的时机暂停、取消、批准或纠正。两者合在一起，Agent 才不是一个只能等待结果的黑盒。

本文是《[把 Harness 讲清楚：Agent 背后的运行系统](https://www.ittt.cc/articles/15)》八个核心部分的第七篇。

## 日志、指标和追踪有什么区别

三者都重要，但解决的问题不同。

### 日志

记录离散事件，适合回答“具体发生了什么”。例如工具参数校验失败、审批被拒绝、模型请求返回限流。

### 指标

记录可聚合数字，适合回答“整体表现如何”。例如任务成功率、P95 延迟、平均工具调用数和每天 token 成本。

### 追踪

把一次任务中的多个阶段串成因果链，适合回答“时间花在哪里、上下游怎样关联”。

一个 Trace 可以包含：

```text
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、缓存命中和停止原因；对工具调用，记录工具名、风险等级、执行环境和结果大小。

敏感参数和模型正文不应默认完整进入遥测系统。可观察性不能以泄露隐私为代价。

## 一次任务的事件流

```text
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%”。更可靠的进度来自真实阶段和证据：

```python
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 包装：

```python
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 才会从黑盒自动化变成可协作的工具。

一句话总结：**让过程看得见，让风险停得下，让用户始终保有最终控制权。**
