AGENT EVALS · DATA ENGINEERING

Trace 即 Evals

Agent 轨迹分析与归因的数据工程实践

张雁飞(Bohu)𝕏2026-06-06

AI 时代焦虑海报

Agent 跑了 3 小时,花了 $12。
失败了,不知道哪一步跑偏;
成功了,也不知道哪一步做对。
下次换模型、改 prompt、加 tool,
效果变好还是变坏,没人说得清。

这已经不是 prompt tuning 的问题,
而是 harness engineering 的问题。

本次分享的目的

降低 Token 浪费 看清哪一步在烧钱、绕路、重复调用
让 Agent 更稳定 找到失败、幻觉、质量波动从哪一步开始
改动有据可依 换模型 / 改 prompt / 加 tool 后,知道到底变好还是变坏
核心方法:记录 Agent 每一步执行轨迹,用数据而不是感觉来优化 Agent。

Agent 工程演进:三个阶段

可靠性的约束结构从文本层迈向系统层,每个阶段包含前一阶段。

Prompt Engineering (2022–2024)
调指令、加示例、写思维链 — 优化单次模型调用的输入文本
代表形态:Chatbot、单轮问答、翻译 / 摘要
Context Engineering (2025)
管理模型每步该看到什么:RAG 检索、记忆压缩、窗口调度 — 优化多轮交互的信息流
代表形态:RAG 应用、多轮对话助手、知识库问答
Harness Engineering (2026)
围绕模型的整套执行基础设施:状态维护、工具中介、反馈注入、约束执行、进度验证
代表形态:Coding Agent、自主任务执行、多 Agent 协作
今天的问题已经不是“prompt 怎么写”,而是“整套 harness 怎么持续变好”。

大家都在 harness 什么?

应用侧:做产品的人

把 Harness 当产品框架 — 让 Agent 跑得更好

模型侧:训模型的人

把 Harness 当考场 — 让模型在里面练得更好

应用侧 Harness:把模型变成可控产品

裸模型 / Prompt 阶段

· 输入一个 prompt,得到一个回答

· 好坏主要看最终输出

· 调优靠改 prompt、加示例

适合 chatbot / 单轮任务

Agent 产品 / Harness 阶段

· 多轮调用 + tool + state + context

· 要控制权限、成本、失败恢复

· 要观测每一步、评估每次改动

Eval 是产品迭代闭环

应用侧 Harness 的目标:让 Agent 可控、可观测、可回归、可持续改进。
而“改进是否生效”要靠 Eval 来判断。

模型侧 Harness:把工具和流程“练”进模型

过去跟模型说怎么用工具;现在让模型在真实 Harness 里反复练,把能力练进权重。

以前:靠外部 Harness “告诉”模型

· 工具用法靠 prompt 描述

· 上下文压缩靠外部逻辑

· 执行策略靠脚手架约束

现在:模型自己“练会”了

· 模型在真实 harness 上 rollout

· 用 trace / reward 学工具调用

· 把上下文管理、执行策略练进模型

仅改 harness 可带来 10× 性能提升

Harness 下沉到模型,不是 Harness 消失了。
而是模型开始在 Harness 里学习,逐步内化工具使用、上下文管理和长期任务执行能力。而 rollout 质量的判断,同样要看 trace。

来源:Cursor × Fireworks · Sequoia Podcast 2026.05

Harness 的难点:改动到底变好还是变差?

应用侧调 Agent、模型侧训模型,最后都要回答同一个问题:这次改动到底是变好了还是变差了?

为什么说不清

· Agent 是不确定性系统,同任务同模型路径也不同

· 只看 pass / fail、只看总成本,看不出差在哪

· 一次 tool 选择、一次上下文裁剪都会改变后续所有步骤

要说清需要什么

· 不能只存终态,要存下每一步的完整过程

· 要能按步骤对比两次执行的差异

· 要能把“哪一步”定位到成本和质量上

两个视角最终汇到同一个东西:Trace。
它既是 Agent 产品的评测证据,也是模型训练的数据燃料;只有展开完整轨迹,才能把“变好还是变差”说清楚。

同一任务、同一模型,换 Harness,轨迹就变了

固定同一个任务和同一个模型(deepseek-v4-pro),只替换 Agent 产品侧的 Harness:

Evot Eval 对比

同一任务的差异

· 成本:evot $0.02 vs claude-code $0.07(便宜 67%

· 耗时:6m10s vs 16m34s(快 63%

· Token:42.9K vs 133.8K(少 68%

· 调用次数:36 vs 75(少 52%

同样完成任务,有的直达,有的绕路。

只看最终结果不够。要展开 trace,才能知道 token 烧在哪、时间慢在哪、哪一步开始绕路。

Demo →

Agent 是路径依赖系统:差异从 Tool Call 开始

一次 tool 选择、一次上下文裁剪、一次错误恢复,都会改变后续所有步骤。展开 trace 后会发现,差异从 tool call 序列就开始了:

Tool Call Sequence Comparison
同一个终态,也可能对应完全不同的执行路径。
所以 Eval 必须从 pass/fail 下钻到 trace。

要找分叉点,先要让 Trace 可计算

Read Read Bash Edit ✓ Edit Bash Read Edit Bash Done Bash Read Read Bash Read Bash Read Edit Bash Read Edit 分叉点:第 4 步 ✓ 直达路径:9 步 · 12K tokens · 18s 第 4 步选 Edit 精准修改目标文件 ↻ 绕弯路径:17 步 · 28K tokens · 35s 第 4 步选了 Bash,输出过多,绕了很多弯才回到正轨
分叉点不会出现在最终分数里。
它藏在完整执行轨迹中:每一步的 tool、context、token、耗时都必须能被存储、查询和对比。

一次发给 LLM 的请求里到底有什么?

模型无状态,client 每次重组上下文

· System:行为约束、工具规则、输出格式

· User task:当前目标和约束

· Messages:历史对话、决策和 compact 摘要

· Tools:工具列表、description、JSON schema

· 工具返回:文件、命令、错误、结构化结果

{ // system instructions "system": "You are an expert coding assistant...", "tools": [ { "name": "Read", "input_schema": {...} }, { "name": "Bash", "input_schema": {...} }, { "name": "Edit", "input_schema": {...} } ], "messages": [ { "role": "user", "content": "fix json parser" }, { "role": "assistant", "tool_use": "Read" }, { "role": "tool", "content": "...command/file/error..." } // old messages may be compacted ], "usage": { "input_tokens": 9200, "latency": "5.4s" } }
大模型侧不保存状态。状态在 client / harness 侧维护;每次调用都要发当前上下文,窗口快满时再 compact、裁剪或重组。 Demo →

Agent Trace 的挑战:长跨度、大 JSON、脏数据

传统 Trace / APM

· 主要记录服务调用链

· 一次请求通常秒级到分钟级

· 字段来自 SDK / instrumentation

· schema 相对稳定

· 分析重点:latency、status、error

Agent Trace

· 一个任务可能持续几十分钟到几小时

· span 持续追加,状态会跨步骤演化

· 内容来自 prompt、messages、tool call、tool result

· LLM 返回经常不是合法 JSON,字段类型会漂移

· 分析重点:token、cost、tool choice、关键分叉点

真正麻烦的不是“存一条链路”。
而是要处理长生命周期任务里的增量事件,把脏的、嵌套的、不断变化的大 JSON 清理、拆解、索引、聚合,并用于归因。

Trace 的难点:把大 JSON 变成可计算数据

一条轨迹的真实规模

· 单条 trace:500KB ~ 500MB

· 嵌套 3~8 层,最多 10万+ span

· 单任务执行最长可达 十几小时

· 每天 几百 TB,只能留存短期

· RL 训练模型自生成的轨迹,比线上流量再大一个量级

从 trace 到可归因数据

· 清理脏 JSON:容错解析模型和工具输出

· 拆解嵌套结构:message、tool call、tool result

· 抽取热点字段:model、tokens、cost、tool、error

· 支持检索和聚合:对比、归因、回放

· 兼容 schema 漂移:agent / tool / model 持续变化

Trace 不是存下来就有价值。只有清理、拆解、索引、聚合之后,才能支撑归因、回放和评测。

Trace 数据层需要哪些数据库能力

需求为什么重要
JSON 原生存取轨迹天然是嵌套 JSON
JSON 清洗与变换脏 JSON、长字段、嵌套结构要在库内处理
JSON 索引加速海量轨迹数据快速检索
JSON 内全文索引根据关键词定位某次对话
JSON Path RBAC合规要求:敏感字段按路径授权 / 脱敏
对象存储,成本低Agent trace 巨大,要长期存
高吞吐持续写入海量用户 / Agent Swarm 并发产生 trace
{
"trace_id": "tr_8f3a2b1c",JSON 加速列
"session_id": "sess_42",JSON 加速列
"model": "deepseek-v4-pro",JSON 加速列
"usage": {
"input_tokens": 4523,JSON 加速列
"output_tokens": 1891,JSON 加速列
"total_cost": 0.047JSON 加速列
},
"tools": [
{ "name": "Read", "description": "Read file..." },
{ "name": "Bash", "description": "Execute cmd..." },
{ "name": "Edit", "description": "Edit file..." }
],
"messages": [
{ "role": "user", "text": "*****" }Path Masking
{ "role": "user", "text": "# LRU File Cache — Spec..." }全文索引
{
"role": "assistant",
"tool_use": "Bash",
"input": { "command": "mkdir -p Sources" }
},
{
"role": "tool",
"type": "tool_result",
"is_error": false,
"content": "{result: ok, files: [...]}"
},
... 48 more steps ...
],
"metadata": {
"agent": "evot",
"task": "lru-cache",
"duration_s": 35.2
}
}

极简 Trace 存储与分析架构

Trace 持续写入对象存储;Databend 通过 Task 消费 Stage,在库内完成入库、清洗和聚合

Trace 生产端
NDJSON
S3 Stage
对象存储
Load Task
入库 + 清洗
events
VARIANT + 加速列
Stream
增量捕获
Aggregate Task
自动刷新
traces
物化聚合
Load Task消费对象存储,入库时去脏 / 截断 / 类型归一
Stream捕获新增 events,驱动后续增量计算
Aggregate Task自动刷新 traces,全流程无需外部调度器

核心实现:events 表 + JSON 清洗 + 增量链路

CREATE TABLE events ( trace_id STRING, type STRING, input_messages VARIANT, output_messages VARIANT, tool_definitions VARIANT, input_tokens INT, output_tokens INT, total_cost FLOAT64, tags VARIANT, -- Computed Column (STORED): 写入时自动物化,查询零开销 input_summary STRING AS (substr(to_string(input_messages), 1, 500)) STORED, output_summary STRING AS (substr(to_string(output_messages), 1, 500)) STORED, ... ) CLUSTER BY (to_yyyymmdd(start_time), trace_id); -- 清洗脏 JSON:截断长 content,保留嵌套结构 SELECT object_insert(raw_data, 'events', json_array_transform(raw_data:events, e -> object_insert(e, 'stringValue', object_insert(e:stringValue, 'content', to_variant(left( CASE WHEN json_typeof(e:stringValue:content) = 'String' THEN e:stringValue:content::string ELSE to_string(e:stringValue:content) END, 200)), true), true)), true) AS cleaned_data; CREATE STREAM events_stream ON TABLE events APPEND_ONLY = TRUE; -- 新 events 到达后,自动刷新 traces 聚合表 CREATE TASK task_refresh_traces SCHEDULE = 5 SECOND WHEN STREAM_STATUS('events_stream') = TRUE AS REPLACE INTO traces ON (id) SELECT trace_id, sum(input_tokens), ...; CREATE INVERTED INDEX idx_search ON events(name, trace_name, operation_name, tags);
VARIANT 原生 JSON · JSON Transform 清洗脏数据 · Computed Column 写时物化 · Stream + Task 自动增量刷新 · Inverted Index 全文检索

总结:先把 Trace 存住、算起来

① 数据先沉下来

不要只看最终 pass / fail

原始 trace 长期保留

② 计算能力跟上

存下来还要查得动、算得快

加速列、全文检索、增量聚合

③ 上层按需构建

Eval / Replay / RL 不再各存一套

一份 trace,多种上层能力

底座:对象存储 + VARIANT + 加速列 + Stream / Task
上层:评测、回放、归因、训练数据
先做好 Trace 存储和计算底座。评测、回放、归因、训练数据都基于同一份数据构建。

试一下?

Databend 正在支撑头部大模型公司的数据底座

辅助 Agent 评测、轨迹归因与 RL 数据管线 · 日均几百 TB 级写入 · 全量 Agent 轨迹持久化

Databend 微信公众号二维码 微信公众号