ancha.

从生产 trace 到测试世界

这一轮(M1 + M2)做完之后,产品能做什么,以及一条 trace 是怎么变成一个能跑的世界的。2026-11-05。

客户的 agent 把生产环境的 trace 发给我们。我们据此还原出它当时所处的世界:原来的 ID、原来的消息、原来的文件都不变。线上那次失败就成了一条能在本地重跑的测试;代码改好之后再跑一遍,就能看到它通过。

做完之后,客户怎么用

接上 trace在 console 里拿一把只能写入的 ingest key,在 agent 的生产环境里设两个环境变量,现有的 OpenTelemetry exporter 就会把 trace 发过来。也可以直接用 ancha import trace.jsonl 上传一个文件。
我们接收、脱敏、存储服务端会去掉认证头和 token,邮箱和电话换成固定的假值(同一个人在 Slack 和 GitHub 里还是同一个人)。原始 trace 默认保留 30 天,在 Traces 页面里每一条都能看到。
自动重建世界每条 trace 生成一个世界,里面有:seed(用原 ID)、过程中出现的人和事件、工具之间的联动、故障规则。每一项都标明是原样还原、推断出来的,还是缺失的。trace 定不下来的地方会变成问开发者的问题,在他回答之前先用默认答案。
拉到本地重跑ancha import 或 ancha pull 把世界拉到本地,再用 ancha run 把线上那次失败重演一遍。代码改好后再跑,看它变绿。
我们自己的运行也能导出ancha runs export --ir 导出的格式和客户的 trace 完全一样,生产记录和沙箱运行可以直接对比。

这一轮做什么,之后做什么

能力状态
GitHub twin 的合并和真实 GitHub 一致:分叉的分支走三方合并,有冲突时返回 405已完成
Ancha 自己的 trace 会记下 webhook 内容和 query;ancha runs export --ir已完成
seed 能固定原始 ID(Slack 的团队、用户、bot、频道、消息 ts;PagerDuty 的用户、服务、升级策略)已完成
服务端重建:IR、OTLP(JSON 和 protobuf)、四个手写读回器、出处和问题已完成
从 twin 自动学映射,覆盖全部有数据格式的 twin(36 个);在每个 twin 上自动验证;没覆盖到的调用回放兜底本轮
收件端(OTLP,支持 JSON 和 protobuf)、只写的 ingest key、脱敏、存储本轮
后台重建 worker;console 的 Connect 页和 Traces 页;ancha import 和 ancha pull本轮
在 staging 上用模拟客户跑通全链路本轮
用 E2B 沙箱托管运行客户的 agent,可以并行跑M3
World 和变体页面、回答问题的界面、变体生成M4
PR 检查、失败聚类、新故障提醒M5
LangSmith 和 Langfuse 连接器;能记录完整请求和返回体的 recorder SDKM6

trace 是怎么变成世界的

trace(OTLP 或文件)→统一成 IR→读回器(每个工具一个)→推断:联动、故障、部署→出处 + 问题→scene 文件→校验,再跑

核心靠代码,不靠 AI。AI 只用来补 trace 里本来就没有答案的那部分,见本节最后。

1. trace 里有什么

trace 记的是 agent 看到的东西:每次 API 调用的请求和返回,以及它收到的每个 webhook。所有来源都先统一成一种格式(Ancha Trace IR),后面只认这一种。

{"kind":"call","t":"…T14:00:01.984Z","service":"slack","method":"POST",
 "url":"https://slack.com/api/conversations.history",
 "request":{"body":{"channel":"C48SKYPN1S3"}},
 "response":{"status":200,"body":{"messages":[{"ts":"1792591080.000000","user":"UFMCYC15CSH","text":"Incident runbook | …"}]}}}
{"kind":"inbound","t":"…T14:01:30.000Z","service":"slack",
 "payload":{"type":"event_callback","event":{"type":"message","text":"Status request: …","ts":"1792591290.000000"}}}

2. 读回:让 twin 自己教我们

twin 本来就是"状态 → API 返回"这样一个函数,读回是它的反函数。我们不为 38 个 twin 逐个手写反函数,而是让每个 twin 自己把它测出来:

  1. 打标记:拿 twin 自带的起始数据,把每个文字字段换成一个独一无二的标记值。
  2. 探测:用这份数据把 twin 起起来,把它所有的读接口都调一遍:先调列表,再顺着返回里的 ID 去调详情。
  3. 学映射:看每个标记出现在哪个接口返回的哪个位置,就知道"返回里的这个字段 = 世界里的那个字段",以及哪些字段是在引用别的实体。
  4. 反过来用:真实 trace 的返回值进来后,按学到的映射原样填回世界,ID 保持原值。
  5. 自动验证:CI 里每个 twin 换一份不同的数据,跑"数据 → 返回 → 读回 → 数据",结果必须一致。twin 一改,映射就重新学。

学出来的映射大概长这样(以 Slack 为例):

trace 里的调用和返回位置填回世界的位置
conversations.list → channels[].name、channels[].id频道的名字和原 ID
conversations.history → messages[].text、messages[].ts历史消息的原文和原 ts
conversations.history → messages[].user引用:发消息的是哪个用户
users.list → members[].real_name用户的全名

有两类数据本身不是一条条记录,自动学不出来,所以保留手写,同时拿来对照学出来的结果:GitHub 的 git 内容(每个版本的文件、diff),以及 Datadog 的时间序列和日志节奏。没被映射覆盖的调用,就按录下来的返回原样回放,并标为 replayed。

3. 推断:世界会怎么反应

4. 出处和问题

状态意思
observedtrace 里看到的,原样还原,并指回来源的那条记录
inferred从模式推出来的,可能错,所以会配一个问题
syntheticagent 从没读过、由生成补上的数据,永远不会覆盖 observed
missingtrace 里没有,也没有补;清单里写明缺了什么

一条 trace 只走过一条路,所以"世界会怎么反应"有时定不下来。有问题的世界照样能跑,只是标成 needs_answers;开发者回答之后再重建一遍。

为什么核心不用 AI

  • 要逐字节准确:ID、时间戳、文件内容、原文都不能变,模型会改,甚至编造。
  • trace 很大:一条 3 分钟的就有 160 KB,全部喂给模型又贵又慢。
  • 要能复现、能审计:同一条 trace 每次重建的结果都必须一样。
  • 按 API 格式还原本来就是机械活,代码做得又快又准。

AI 用在哪

  • 补上 agent 没读过的那部分世界,标为 synthetic。
  • 把"到点发一条消息"的人变成会对话的人:从他们发过的消息里提取事实和说话方式。
  • 生成变体:换说法、换时机、换工具返回。
  • 起草检查项,以及问开发者的问题。
  • 客户的 trace 只有自定义 tool 层时,帮忙把 tool 对应到 API。

用便宜的模型,生成的东西都标为 inferred 或 synthetic。

已经验证过的

25 / 33只凭一条 trace 原样还原的世界项;另有 3 项近似,5 项缺失,缺失的都是 agent 从没读过的
3 + 3同一个 agent 在原世界和重建世界里各跑 3 次:动作顺序一致,结果分布也一致
0模拟客户通过 OpenTelemetry 发来 61 条记录,在里面扫描密钥和认证头,命中为 0

现在还做不到的