我们如何实现大规模持续追踪智能
How we made continuous trace intelligence possible at scale
Braintrust 于2026年6月4日发布Topics,一种用于大规模智能分析agent trace的解决方案。Topics通过六阶段pipeline处理trace:预处理将原始trace转换为token(硬限制128K token),facet提取由LLM(Gemma模型,在Baseten上服务)生成简短摘要,embedding生成1024维向量,HDBSCAN聚类(用Rust编写,约10秒),LLM命名聚类,最后通过embedding查找进行分类(每trace约100毫秒,无LLM调用)。Topics支持Task、Sentiment、Issues等内置facet及用户自定义facet,分类结果作为结构化列可被SQL查询。
2026年6月4日 Ankur Goyal 16分钟
如果你在生产环境中运行一个 agent,你一天中的部分时间可能都在翻阅海量的生产日志,看看有没有什么值得注意的东西。你知道其中肯定藏着有用的情报,但数据本身不会告诉你该问什么问题,或者用什么 SQL 查询能把它抓出来。
我们在 Braintrust 思考这个问题已经很久了,Topics 就是那个让你能以规模化智能分析 trace 的解决方案。和 Brainstore 一样,这是我们为数不多的、集中押注的技术赌注之一——我们选择端到端地拥有一个原语,而不是拼凑现成的组件。对于 Brainstore,赌注押在存储和查询层。对于 Topics,赌注则押在位于其上的智能层。
什么让 trace 分类变得困难
为了发现你原本不知道要寻找的模式,你需要在这个智能层上运行每一个 trace。我们称之为主动可观测性。但让一个 coding agent 处理你 100% 的生产流量成本太高了,所以任务就是将其提炼成少数几个值得关注的东西。成本是障碍,因为 LLM trace 的形状不符合标准工具所期望的任何形式。
标准的 NLP 工具包假设文档的形状和大小大致均匀。使用 LDA 的主题建模期望的是几百个 token 的词袋文档。情感分析期望的是一个句子或一个段落。现成的 embedding 聚类期望输入能放进 embedding 模型的上下文窗口,而今天这个窗口的上限大约是 8,192 个 token。
Agent trace 不是那样的。一个生产环境中的 trace 可能包含数百万个 token 的对话历史、工具调用、中间推理、检索到的上下文和序列化的应用状态。它们以高流量到达,在“完成”后还会持续更新,而有价值的信号通常埋藏在数百个 span 中的几个里。如果你对原始 trace 做 embedding,你会得到被消息长度或工具名称频率等表面特征主导的嘈杂聚类。如果你先用 LLM 做摘要,你会超出预算。如果你激进地采样,你会错过长尾,而 bug 通常就藏在那里。
还有一个方法论问题。团队希望对同一份日志提出三个不同的问题:收到了什么类型的请求、出了什么问题、用户对回复感觉如何?这通常需要三个不同的技术栈。第一个用主题建模,第二个用错误挖掘或异常检测,第三个用情感分析。维护三个 pipeline,每个都有自己的预处理和自己的失败模式,对应用 AI 团队的时间来说并不是很好的利用。
Topics 之前的问题解决方式
在我们构建 Topics 之前,团队在实践中回答“我的日志里发生了什么”的方式通常表现为三种模式之一,而我们观察到它们都以类似的方式失效。
第一种模式是手动分类。工程师会打开日志视图,按日期过滤,按分数排序,然后开始阅读。这在规模较小时有效,但一旦每天超过几千个 trace,就完全行不通了。
第二种模式是抓取少量 trace 交给一个 coding agent。挑几个看起来有趣的例子,粘贴进去,然后问出了什么问题。这很快,而且出奇地有用,但它只能看到你能负担得起粘贴的那几个,而且是一次性的。一旦流量变化或产品改变,你就得从头再来。
第三种模式是为每个项目构建一个自定义分类器。选择一个分类体系。写一个 prompt。把它作为 LLM-as-a-judge 评分器在每个 trace 上运行。这有效,但在规模上很昂贵,分类体系在产品变化时就会过时,而且你无法发现你还不知道要问的新类别。
重新思考架构
驱动 Topics 的洞见受到了 Anthropic 的 Clio 论文 的启发。与其尝试对原始 trace 做 embedding 或分类,不如让 LLM 做一件事:用一两句话沿着单一维度总结 trace。然后你对这个摘要做 embedding,对 embedding 进行聚类,再用第二轮 LLM 调用为聚类命名。trace 本身永远不需要适合 embedding 模型的上下文窗口。下游 pipeline 永远不需要知道任何关于 agent、工具或 token 计数的事情。
这听起来像是一个小改动,架构上也是如此。但在操作上,它改变了一切。一旦 LLM 摘要步骤存在,同一个下游 pipeline 就可以用于你关心的任何维度。任务、问题、情感、你为产品定义的自定义类别,都流经相同的 embed、cluster、name、classify 阶段。
它也改变了成本结构。pipeline 中昂贵的部分是 LLM 摘要。下游的一切都很便宜。所以只要你做好摘要,并且每个 trace 只做一次,你就可以持续地对每个新 trace 进行分类,而不会超出预算。
这两个观察——先摘要再 embedding 和统一的下游 pipeline——是 Topics 背后的架构赌注。
架构目标与策略
从这个赌注中衍生出三个设计目标。
首先,LLM 摘要步骤必须范围紧凑、对批处理友好,并且足够便宜以在每个 trace 上运行。这个步骤的输出是一个 facet,它是整个 pipeline 围绕构建的工作单元。
其次,聚类生成步骤必须足够快,以便无需操作员干预即可临时运行,并且生成的主题映射必须足够稳定,使得跨运行的趋势分析有意义。生成式命名在运行之间总会漂移,因此持久的身份单元是聚类,而不是名称。
第三,分类必须足够便宜以持续运行。这意味着在分类时没有 LLM 调用。唯一的操作是针对保存的主题映射的质心进行 embedding 查找,我们可以在每个 trace 大约 100 毫秒内完成。
从这些目标中产生的 pipeline 有六个阶段。

阶段 1,预处理
预处理器将原始 trace 转换为 token。默认的预处理器遍历范围内的每个 span,将每个 span 解析为 LLM 对话,跨 span 去重消息,并丢弃 scorer span。自定义预处理器是返回文本、文本数组或 null(以跳过该 trace)的 JavaScript 函数。这些对人类或 LLM 来说很容易编写,因为它们只是将 span 中的数据移动到所需格式。
这个阶段存在只有一个原因。原始 trace 可能非常庞大,而 facet 模型有一个有限的上下文窗口。预处理通常每个 trace 远低于一秒完成,并且在到达 facet 模型之前,输出被硬限制在 128K token。这也是我们与 Clio 最大的不同之处。Clio 总结的是已经以大致均匀形状到达的对话,而我们必须将任意的、庞大的 agent trace 转换成单个 LLM 调用可以处理的东西。
阶段 2,facet 提取
facet 阶段是 LLM 完成其单一任务的地方。每个 facet 都有一个 prompt,例如“用一句话总结用户试图完成什么”,以及一个输出 schema。facet 模型生成一个简短的文本块,通常是一两句话,并将其写回 trace 作为 facets.<FacetName>。

内置的 facet 是 Task、Sentiment 和 Issues。自定义 facet 是用户定义的 prompt,可以是任何内容,从用例到 SKU 细分。
这个阶段有一个对成本非常重要的优化。一个朴素的实现会一次计算一个 facet,因此在一个 trace 上运行五个 facet 的成本大约是 trace token 的五倍加上五个 facet prompt。Prompt 缓存可以减轻一部分,但在这种规模下仍然很昂贵且更难做对。我们将多个 facet 批处理到单个 LLM 调用中,因此成本是 [trace tokens] + [# facets] × [facet prompt tokens]。无论你运行多少个 facet,trace token 只支付一次。五个 facet 的成本不是单个的五倍。
facet 模型是一个由 Braintrust 管理、在 Baseten 上服务的模型。我们目前使用 Gemma,因为它在与其他小模型相比时表现最佳。每次 facet 运行平均大约需要 10 秒。由于我们以异步批处理方式运行,在高峰期可能需要长达 60 秒,这对于一个持续的后台 pipeline 来说是可以接受的。
阶段 3,embedding
在 facet 文本生成后,我们使用我们的 embedding 模型(也在 Baseten 上服务)对其进行 embedding。输出是一个 1024 维的稠密向量。
这里需要注意我们 embedding 的是什么。是 facet 输出,而不是原始 trace。这就是使 pipeline 其余部分易于处理的原因。一个一致的、简短的、切题的摘要可以干净地做 embedding。一个百万 token 的 agent trace 则不行。
该向量与 trace 上的 facet 文本一起存储在 Brainstore 中,并根据项目的保留策略进行垃圾回收。
阶段 4,聚类
一旦积累了足够多的 facet embedding,聚类阶段就会在每次生成过程中对最多 50,000 个 facet 的样本运行。默认算法是使用 UMAP 进行降维的 HDBSCAN。K-Means 和 Hierarchical 可作为替代聚类算法,PCA 可作为替代降维方法。
我们选择 HDBSCAN 作为默认算法有两个原因。它不需要你预先指定聚类数量,这很重要,因为正确的主题数量是你的数据的函数,并且会随时间变化。而且它自然地将异常值识别为噪声,而不是强制将每个点都归入某个聚类,这与真实流量的分布方式一致,即围绕少量重复模式的长尾一次性请求。
对于每个聚类的关键词提取,我们使用 c-TF-IDF,这与 BERTopic 推广的方法相同。一次对数千个 trace 的生成过程大约在 30 秒内完成。其中大约 10 秒是聚类,用 Rust 编写。其余是一系列用于命名聚类的 LLM 调用。
阶段 5,命名
命名阶段获取每个聚类的代表性 facet 样本,并要求 LLM 生成一个简短的名称和描述。这是生成式的,这意味着即使底层成员几乎没有变化,同一个聚类在下一次生成过程中也可能获得略有不同的名称。一个重要的经验是,你必须使用大型模型并同时命名多个聚类,否则名称的区分度不高。这就是为什么我们将聚类(而不是名称)视为稳定的身份。当生成新的主题映射时,我们会自动将相似的聚类与其前身匹配并重用它们的 id。

阶段 6,分类
分类是便宜的部分。对于每个新 trace,我们运行预处理、facet 和 embedding 阶段,然后在保存的主题映射中查找最近的聚类质心。如果 trace 在配置的距离阈值内,它就会获得一个标签。如果不是,我们会写入 no_match,而不是强制赋予一个错误的标签。
在分类时没有 LLM 调用。整个步骤每个 trace 大约需要 100 毫秒,这使得在采样后可以在 100% 的流量上运行。
结果作为 classifications.<TopicMap>[0].label 落在 trace 上,同时 embedding 距离和聚类索引作为元数据。从那时起,它的行为就像日志中的任何其他列一样。你可以对其进行过滤、分组、跨主题映射连接,或从 SQL 查询。
sql
SELECT classifications.Task[0].label as topic, count(*) as countFROM project_logs('my-project-id')WHERE classifications.Task IS NOT NULL AND created > now() - interval 7 dayGROUP BY topicORDER BY count DESC
这个 SQL 模式就是我们所说的 Topics 输出是可查询的。分类结果作为一列落在 trace 上,这意味着你可以对其进行过滤、分组、设置告警,或与日志中的任何其他内容进行连接。完整的查询模式记录在 SQL 参考 中。
将它们联系在一起的自动化
Topics 的另一半是自动化层,它在后台持续运行 pipeline,具有合理的默认值和正确的状态机。
一个 topic 自动化是一个小型的、在四个状态间移动的状态机。它从 waiting_for_facets 状态开始,在此状态下积累 facet 数据。一旦有足够的数据,它就会转换到 recomputing_topic_maps 状态,运行聚类生成过程,并生成一个新的主题映射。从那里它进入一个回填状态,用新的主题映射对最近的 trace 进行分类,最后进入 idle 状态,直到下一次计划运行。
有两个值得了解的阈值。一个项目中至少需要 400 个 trace 才能启动 Topics,并且至少需要 100 个唯一的 facet 摘要才能生成主题映射。低于这些数字,聚类就没有意义,我们宁愿什么都不显示,也不愿显示噪声。
在产品中有两个地方你会看到聚类。
Topics 视图显示自动化的主题映射。同一组持久的聚类,相同的名称,随着时间的推移一致地应用于你的日志。这就是你进行趋势仪表板、告警以及任何需要在几天或几周内进行比较的分析时所需要的。
在过滤视图上的“按 facet 聚类 trace”操作会在你当前查看的任何子集上运行临时聚类,参数针对探索进行了调整。你得到的聚类特定于该切片。在昨天的失败上运行,在特定的用户群上运行,在单个实验上运行。这是你在调查(而非监控)时所需要的。
两个视图使用相同的 pipeline。区别在于主题映射是持久化并重用,还是即时生成。
模型、托管和数据流
所有原生的 Topics 推理都在 Baseten 上运行。这包括 facet 提取、embedding 生成和主题命名。Baseten 对推理负载实行零数据保留,因此我们发送用于推理的输入和输出不会被存储。
我们对于发送什么内容是有意为之的。facet 模型看到的是预处理后的文本,其本身被限制在 128K token,并且去掉了附件和指标。embedding 模型只看到 facet 输出,从未看到原始 trace。聚类步骤操作的是 embedding 和 facet 输出,从未操作原始 trace 内容。
Topics 推理可以在美国和欧盟托管,并且我们符合 HIPAA 标准。在 BYOC 部署中,你的 trace 数据保留在你的数据平面内,而 Topics 原生模型调用会出站到 Baseten。组织管理员可以完全禁用原生模型,这将在整个组织中关闭 Topics。
Topics 默认在原生模型上运行。facet 步骤特意设计为开箱即用,因为大部分质量工程都在这里,并且 prompt 与模型是协同调优的。对于命名,你可以选择你喜欢的模型。
架构即开发者体验
上述架构选择转化为一些具体的属性,你作为开发者会感受到。
pipeline 持续运行,而不是作为批处理作业。分类仅使用 embedding,并在每个匹配的 trace 到达时运行。主题映射默认每天重新生成一次,节奏可配置。你不需要启动一个作业,不需要等待夜间运行,在大多数项目中,你在 trace 上看到的标签在摄入后几分钟内就会更新。
输出是可查询的,而不仅仅在 UI 中可用。分类结果作为结构化列存在于 trace 上,并且可以通过 SQL 访问。一个 trace 的主题是一个 WHERE 子句,你可以在此基础上构建、设置告警,或与其他 facet 连接以回答诸如“本周情感为负面的首要任务是什么”之类的问题。
一个 pipeline 服务于你关心的每一个维度。添加一个自定义 facet 并不意味着要连接新的聚类、新的命名或新的分类。下游阶段不关心你沿着哪个维度提取。一个新 facet 的成本是一个 prompt 的成本加上 facet prompt token 的边际成本,背后没有新的 pipeline。
身份在重新生成之间保持稳定。主题映射会重新生成,名称会漂移,但底层的聚类身份在运行之间是匹配的,因此你的仪表板和保存的查询可以继续工作。你所依赖的东西——聚类——是稳定的。可能漂移的东西——名称——被视为一个标签。
Topics 是开箱即用的,当你需要时,逃生舱口就在那里。内置的 facet 和默认的预处理器是为大多数项目开箱即用而设计的。当你的 trace 形状不寻常时,例如一个多 agent 设置,默认的线程预处理器引入了太多 scorer 输出,你可以插入一个自定义的 JavaScript 预处理器,它只返回你想要聚类的 span。其背后的完整 pipeline 是相同的。
如果你想更详细地了解功能范围,Topics 文档 涵盖了产品,自定义 facets 介绍了如何编写你自己的 facet,管理 Topics 涵盖了自动化控制。处理分类的 SQL 模式在 SQL 参考 中。
Topics 是我们公开讨论过的第二个赌注,和 Brainstore 一样,随着我们产品的成长,它变得越来越有用。我们认为它是你可以运行的最通用的基线智能层,足够便宜以应用于你 100% 的 trace。我们正在构建的更昂贵、更强大的层位于这个基线之上。背后的团队很小,功能范围比看起来要大,而且我们认为随着生产 AI 工作负载变得越来越复杂,这个 pipeline 的形状将继续有用。