智能体轨迹压缩成自动机,这个话题在我最近一次框架选型实测里直接派上了用场。把一批 agent 运行轨迹压缩成自动机之后,我发现一个非常直观的结论:行为更多由框架决定,而不是由模型参数或提示词细节决定。这篇文章把怎么做、需要看哪些参数、哪些地方最容易踩坑,按我实际落地的顺序拆一遍。适合准备做 agent 框架选型、多智能体调试,或者想用自动化方法分析 agent 行为稳定性的开发者看。
1. 轨迹压缩成自动机之前,先搞清楚这三个概念
1.1 轨迹是行为序列,不是日志文本
在智能体场景里,一条轨迹是指一次任务从开始到结束的完整行为序列,通常包含用户输入、模型调用、工具调用、观察结果、中间推理、最终答案这些元素。普通日志是按时间顺序刷出来的文本,单看一条还好,几十条上百条叠在一起,很难说清楚“这个 agent 到底是怎么做事的”。
轨迹强调顺序和因果。先调用搜索工具再给结论,和先给结论再调用搜索工具,日志里都能写出来,但在行为语义上是完全不同的。前者是“查了再说”,后者是“编完再补依据”。如果你只看日志文本,这两者可能混在一起;但一旦转成轨迹序列,差别立刻显现。
我一般会先把轨迹统一成动作序列,每个动作只保留“类型 + 关键对象”,不保留具体内容。比如用户问了一个问题,中间动作就是 USER_INPUT、LLM_CALL、TOOL_SEARCH、OBSERVATION、LLM_CALL、FINAL_ANSWER。至于问题里的具体实体名称、搜索关键词,先剥掉。这一步很重要,因为自动机建模的是行为结构,不是业务内容。
1.2 自动机是轨迹的行为骨架
自动机在这里就是一张有向图,节点是状态,边是转移关系。把一批轨迹叠在一起,相同的动作模式合并成一个状态,转移上标记出现频次,就得到自动机。
它的价值不是复述某一条轨迹,而是把一组轨迹的共同结构抽出来。假设你收集了 100 条成功轨迹,其中 80 条都是“先调用搜索工具,再调用代码工具,最后输出答案”,自动机上就能看到一条主通道,从起始状态经过搜索状态、代码状态,再到终止状态,转移频次很高。剩下 20 条走了别的分支。这个骨架比 100 条日志直观得多。
实现上,常见做法是从前缀树开始合并。先把所有轨迹按动作符号化,建一棵 trie,再把结构等价、语义等价的状态合并,最后按转移频次剪枝。如果只想做行为模式匹配,也可以借用 AC 自动机那套多模式匹配思路,去扫描哪些高频动作片段反复出现。但我不建议一上来就上复杂方法,先用前缀树加状态合并,够用且容易解释。
1.3 框架是轨迹结构的第一个约束条件
智能体框架解决的核心问题,是把模型调用、工具调用、记忆读写、任务分发这些环节串起来。串的方式不同,行为空间就不同。
这句话是理解“行为更多由框架决定”的关键。框架不是透明的执行器,它本身就是行为空间的边界。你在一个不允许循环的框架里,永远压不出带循环的自动机;你在一个默认重试 3 次的框架里,自动机上大概率会出现一条反复调用同一工具的分支。所以在分析轨迹之前,先把框架的控制流结构弄清楚,否则很容易把框架造成的现象误判成模型或提示词的问题。
2. 为什么行为更多由框架决定:从自动机形态看
2.1 框架先定义了路径,模型只做局部选择
不同类型框架,压缩出来的自动机形态差异非常明显。
偏工作流平台的框架,比如 Dify 这类,agent 行为是用节点图定义的,开始节点、LLM 节点、工具节点、结束节点,连线固定。轨迹基本是线性或少量分支,不太可能出现“无限循环调用自己”这种结构,因为图本身限制了路径。压缩出来的自动机,主干清晰,节点少,分支少,确定性高。
典型 ReAct 风格的 agent 框架,循环结构是内建的:思考、行动、观察、再思考。轨迹天然会出现重复的“THINK → ACTION → OBSERVATION”环,环数取决于任务复杂度和最大迭代次数。自动机上会表现为明显的自环或回边,这是框架设定好的,不是模型随机跑出来的。
多智能体框架更不一样。轨迹里会出现跨智能体消息,比如 AGENT_A_TO_AGENT_B,状态图会分出很多子图,消息传递顺序往往由框架的调度策略决定。你甚至能通过自动机直接看到哪个智能体是消息中心,哪个智能体是边缘节点。
把这些自动机放在一起看,主干结构几乎就是框架控制流图的实例化。模型能力、提示词、温度这些因素,更多是在同一个结构里做局部选择,很难突破框架给定的路径。
2.2 实测对比:换框架比换提示词更容易改变骨架
我做过一个对比实验:同一个模型,同一组任务,分别在偏工作流框架和偏自由 ReAct 风格框架里跑,然后各自压缩自动机。
结果很明显。工作流框架的自动机接近一条线性管道,节点顺序固定,几乎不存在回边。ReAct 框架的自动机则带明显的循环回溯分支,失败时会弹回之前的思考状态重新选路。两者骨架完全不同。
反过来,在同一个框架里换提示词、调温度,自动机主干几乎不变,变的最多只是某些分支上的动作组合和失败回退次数。这说明一个问题:分析 agent 行为时,框架是最高优先级的变量。如果你看到某个 agent 频繁出现某类行为,先别急着改提示词,先看看是不是框架把路径限制成这样,或者框架默认就循环执行了 N 轮。很多所谓的“行为问题”,本质是“框架配置问题”。
3. 环境与数据准备:没有干净的轨迹,压缩无从谈起
3.1 轨迹来源:平台日志、埋点、trace 和回放
要压缩自动机,第一步是拿到足够多、字段完整的轨迹。常见来源有四种。
第一,agent 平台自带的运行日志或调试面板导出。Dify、Coze 这类平台一般都能看到单条任务的事件流,直接导出或用接口拉取。
第二,自己业务代码里埋点。在每次 LLM 调用、工具调用、消息传递的地方写日志,记录会话 ID、步骤序号、动作类型、时间戳。这是最灵活的方式,也是我推荐长期使用的方式。
第三,通过可观测性工具收集 trace 数据。如果你的服务已经接了 OpenTelemetry 之类的链路追踪,agent 调用链天然就是轨迹,只需要做一次格式转换。
第四,测试集回放。把一组固定问题反复跑,记录每次的行为序列。这种方式适合做对比实验,因为输入可控。
我建议至少收集 50 到 100 条有效轨迹再开始压缩。少于 20 条,压缩出来的自动机基本都是单路径,看不出行为分布,意义不大。样本要同时覆盖成功和失败两种结果,因为失败路径往往是框架问题最明显的暴露点。
3.2 统一轨迹格式:先转 JSON Lines
不同框架日志格式差异很大,直接拿来分析会乱。建议统一转成 JSON Lines 格式,每个事件一行。下面是我常用的字段结构,你可以按实际日志调整:
{"session_id": "task_001", "step": 1, "actor": "USER", "action_type": "INPUT", "detail": "用户问题", "timestamp": 1700000000, "status": "success"} {"session_id": "task_001", "step": 2, "actor": "AGENT", "action_type": "LLM_CALL", "detail": "模型调用", "timestamp": 1700000001, "status": "success"} {"session_id": "task_001", "step": 3, "actor": "TOOL", "action_type": "TOOL_CALL", "detail": "search", "timestamp": 1700000002, "status": "success"} {"session_id": "task_001", "step": 4, "actor": "SYSTEM", "action_type": "OBSERVATION", "detail": "搜索结果摘要", "timestamp": 1700000003, "status": "success"} {"session_id": "task_001", "step": 5, "actor": "AGENT", "action_type": "FINAL_ANSWER", "detail": "最终回复", "timestamp": 1700000004, "status": "success"}字段含义:
- session_id:哪一次任务;
- step:第几步,用于还原顺序;
- actor:USER、AGENT、TOOL、SYSTEM;
- action_type:INPUT、LLM_CALL、TOOL_CALL、OBSERVATION、FINAL_ANSWER、ERROR;
- detail:动作的补充描述,压缩阶段一般不用,排查时再看;
- timestamp:时间戳,判断重试间隔和循环耗时;
- status:success、failed、timeout,用于区分正常路径和失败路径。
最终压缩只用到会话 ID、步骤、actor、动作类型、工具名这几个字段,但其他字段在排查时很关键,不要提前删掉。
3.3 工具链选择:pandas、networkx、graphviz 足够
直接用 Python 就能做,不需要重型平台。我通常只依赖几个库:pandas 做数据清洗,networkx 建图和统计转移,graphviz 或 pyvis 出图,再加一个 json 或 yaml 做配置。没有固定版本要求,按你的 Python 环境正常安装即可。
如果你的轨迹量到了几万条,可以把 pandas 换成 polars,处理速度会快不少。但第一次做完全没必要,先把方法跑通,再考虑性能优化。
注意:这一步最容易忽视的是埋点规范。如果不同模块记录的工具名不统一,比如 search_web、web_search、google_search 混着出现,后面清洗会非常痛苦。先花半小时把命名统一,比任何算法都重要。
4. 从轨迹到自动机的完整流程
4.1 符号化:把轨迹变成动作序列
这一步的目标是把每条轨迹变成一行符号数组。先按 session_id 分组,按时间戳排序,再映射成符号。
比如一条轨迹:
用户输入 → 模型调用 → 调用搜索工具 → 返回结果 → 模型调用 → 最终答案符号化之后就是:
USER_INPUT, LLM_CALL, TOOL_SEARCH, OBSERVATION, LLM_CALL, FINAL_ANSWER如果工具调用有多个参数,我只保留 tool_name,参数留到原始日志里查。原因前面说过:自动机建模的是行为结构,不是参数细节。参数一旦进入状态,状态数会爆炸。
代码示意如下,字段名以你自己日志为准:
symbols = [] for event in session_events: if event["actor"] == "USER": symbols.append("USER_INPUT") elif event["actor"] == "AGENT" and event["action_type"] == "LLM_CALL": symbols.append("LLM_CALL") elif event["actor"] == "AGENT" and event["action_type"] == "FINAL_ANSWER": symbols.append("FINAL_ANSWER") elif event["actor"] == "TOOL": symbols.append(f"TOOL_{event['detail'].upper()}") elif event["actor"] == "SYSTEM" and event["action_type"] == "OBSERVATION": symbols.append("OBSERVATION") elif event["status"] in ("failed", "timeout"): symbols.append("ERROR")特别注意:不要把 ERROR 和 OBSERVATION 混在一起。出错状态必须单独保留,因为自动机里是否出现 ERROR 状态,以及错误之后回到哪个状态,是判断框架稳定性的核心指标。
4.2 建前缀树并合并等价状态
有了符号序列集合,第一步是建前缀树。把每条序列按顺序插入树中,相同前缀共享路径,每个节点记录经过它的轨迹数量。
这一步的直观作用是:直接看到所有轨迹的共同开头是什么。比如大部分轨迹都是 USER_INPUT → LLM_CALL 开头,说明框架统一先做一次模型调用,而不是直接调工具。如果有些轨迹是 USER_INPUT → TOOL_CALL 开头,说明框架支持跳过模型直接执行工具,这个分支值得关注。
建完树之后做状态合并。合并原则不是看状态名一样,而是看语义等价。比如 LLM_CALL 后接 TOOL_SEARCH,和 LLM_CALL 后接 TOOL_WEB_FETCH,如果你只是分析框架行为,这两个搜索类工具状态可以合并成 TOOL_RETRIEVAL。合不合并,取决于你想分析到什么粒度。
如果你分析的是动作模式的共现,可以先用序列挖掘方法找出高频动作片段,再把高频片段作为自动机的候选子路径。这个思路在数据量大时更快,但解释性稍微弱一点。第一次做,我建议老老实实用前缀树加人工确认的合并规则。
4.3 统计转移、剪枝和出图
状态合并完成后,统计所有相邻转移的次数。比如从状态 A 到状态 B 出现了 80 次,从 A 到 C 出现了 20 次,就在图上画两条边,标上权重。
这一步建议输出两个东西:
- 转移表:CSV 格式,包含 from、to、count、ratio,方便后续用脚本过滤和排序;
- 可视化图:networkx 生成,标注主路径和分支,方便人眼判断。
判断主路径的方式很简单:从起始状态开始,沿着每个节点上转移比例最高的边走,直到终止状态。这条主路径就是框架下最常见的行为模式。分支多不多,看每个非终止节点是不是都存在明显的次要转移。
剪枝时不要只按绝对次数砍。比如总共只有 50 条轨迹,出现 2 次的边占比 4%,这种边界信息有时能暴露框架的异常分支。我建议先保留所有边,出图时再按最小支持度过滤,不要把原始转移表里的数据也删掉。
5. 四个关键参数与三套判断标准
5.1 参数表:支持度、合并阈值、截断长度、状态集合
压缩自动机没有唯一算法,参数不同,结果差异很大。我常用这几个参数:
| 参数 | 作用 | 建议初值 | 说明 |
|---|---|---|---|
| 最小支持度 | 转移频次低于该阈值的边不画出 | 5% 或 3 次 | 太低会有一堆噪声分支,太高会丢掉关键边界行为 |
| 状态合并阈值 | 决定哪些状态可以合并 | 先按动作类型精确合并 | 不要一上来就做语义合并 |
| 最大轨迹长度 | 超过 N 步的轨迹是否截断 | 按框架最大迭代轮数定 | 防止异常长尾干扰结构 |
| 是否允许未知状态 | 是否允许自动机出现未定义状态 | 建议允许 | 未知状态是观察新行为的窗口 |
最小支持度最影响美观,状态合并阈值最影响结构,最大轨迹长度最影响环的判断,固定状态集合最影响扩展性。这四个参数要分开调,不要同时乱改。
5.2 覆盖率、确定性和主路径占比怎么用
四个指标我每次必看:
- 覆盖率:自动机上的主路径和主要分支能覆盖多少原始轨迹。低于 70%,说明状态抽象或阈值有问题,骨架没有反映真实行为。
- 确定性:给定一个状态,它的下一转移是否主要集中在一条边上。如果某状态后面均匀分到 5 条边,说明这个位置行为非常不稳定,需要放大看是框架随机还是提示词导致。
- 环的数量和位置:出现自环是正常的,比如 ReAct 的多轮迭代。但如果某个非预期节点出现大量自环,比如反复调用同一个工具,多半是框架循环配置或工具返回异常。
- 主路径占比:主路径覆盖轨迹的比例。比例高说明行为稳定,比例低说明行为分散,可能是任务多样性太大,也可能是框架没有收敛。
这四个指标不是越高越好。覆盖率想拉高,就把状态合并得粗一点;想凸显细节,就降低合并粒度。关键是你得明确这次压缩的目的是看主干还是看异常。
5.3 参数调优的先后顺序
不要一上来就调语义合并。我建议的顺序是:先用精确动作类型压一版,看主路径和覆盖率;再决定是否合并同类型工具;最后才是引入内容哈希或语义相似度。每调一步,重新看覆盖率、确定性和主路径占比。三个指标同时变差,说明抽象过度了。
注意:状态合并阈值和最小支持度是两个不同概念。前者决定状态是否等价,后者决定边是否保留。只调后者不调前者,通常只能去掉细枝末节,不能改变结构。
6. 常见问题和排查顺序
6.1 样本太少,压缩结果全是单路径
现象:自动机只有一条从头到尾的线,分支几乎没有。先别高兴,这不代表框架很稳定,很可能是样本量太小,还没来得及出现分支。
排查链路:
- 先看样本数量,少于 30 条直接补数据;
- 再看任务多样性,如果 30 条都是同一个问题,压缩结果自然单薄;
- 最后看是否过滤了失败轨迹,如果只保留成功轨迹,失败回退分支会被过滤掉。
我一般会先把成功和失败轨迹分开压缩一次。成功轨迹的自动机看主流程,失败轨迹的自动机看崩溃点,两个结合起来才能定位问题。
6.2 状态爆炸:先查符号化,再查归一化
现象:自动机节点几百个,根本没法看。最常见原因是把不该进状态的内容放进去了,比如把具体参数、完整输入文本、工具返回值当成了状态标识。
排查链路:
- 先确认符号化时是否只保留 actor 加 action_type 加 tool_name;
- 再确认是否对不同工具名做了归一化,比如 search_web、web_search、google_search 要合并成一个状态;
- 然后看是否插入了内容哈希,如果插了,去掉再看;
- 如果还是爆炸,改用最小支持度过滤低频节点。
90% 的状态爆炸都是前两步造成的,很少需要动用复杂算法。
6.3 环太多:区分标准循环和失败重试
现象:图里到处都是回边,画出来像蜘蛛网。这通常不是状态合并问题,而是 agent 在真实任务里反复重试或反复决策。
排查链路:
- 先看具体是哪些边形成环,是 THINK → ACTION → OBSERVATION 这种标准推理循环,还是 ACTION → ERROR → ACTION 这种重试循环;
- 标准循环,看框架最大迭代轮数是否合理,默认 10 轮不代表任务需要 10 轮;
- 重试循环,看工具调用是否稳定,很多环是工具超时或返回格式错误导致模型反复重来;
- 检查每条环的轨迹耗时和 token 消耗,判断是否值得优化。
这里最容易误判的是把失败重试的环当成正常迭代。区分方法很直接:看环上的转移是否经过 ERROR 或 TOOL_ERROR 状态。经过了,就是重试;没经过,才是正常推理循环。
6.4 框架差异看不出来:比较低频分支和边界行为
现象:跑了两套框架,压缩出来的自动机主路径高度相似。先检查是不是两个框架本质上用了同一种 agent 循环范式。比如两个框架都内置了 ReAct 循环,主路径当然类似。
这时候要比较的不是主路径,而是分支和边界行为:
- 比较起始状态:一个框架是否先做系统指令注入,另一个是否直接调模型;
- 比较结束状态:失败时是否走统一的 ERROR 状态,还是直接返回半成品;
- 比较多智能体场景下的消息边:框架 A 是串行转消息,框架 B 是直接调用函数,自动机形态会不同;
- 把阈值降低到 2%,看低频分支的差异。低频分支才是框架细节的暴露区。
主路径决定框架的“大类”,低频分支和错误处理决定框架的“细节”。选型时两个都要看。
7. 这套方法的适用边界和落地建议
7.1 适合做框架选型、回归测试和稳定性分析
这套方法更适合做横向对比和稳定性分析,而不是单点性能调优。典型场景包括:
- 框架选型:两个 agent 框架,用同一模型同一测试集,各自压缩出自动机,直接比较行为结构和失败分支;
- 回归测试:修改框架配置或升级框架后,重新压缩轨迹,看主路径和覆盖率是否变化;
- 提示词分析:确认在同一个框架内,提示词改动是否真的改变了行为结构,还是只在局部改了几个分支;
- 多智能体调度分析:看智能体之间消息传递的路径和频率分布,判断是否存在消息风暴或单点瓶颈。
7.2 不适合单条调试和严格自动机学习
如果只是单条任务调试,或者任务数量很少,直接看日志更快,不用压缩。自动机是统计结构的工具,样本太少时输出没有说服力。
如果你想追求自动机本身的数学完备性,需要严格的自动机学习算法和验证集,那本文的前缀树合并方法不够严谨,只能算行为骨架。
另外,如果轨迹里动作类型没有统一规范,工具名五花八门,第一步清洗就要花大量时间。先做好埋点规范,比选任何算法都重要。
7.3 落地前先做好埋点和输出规范
最后留几个我自己的经验点。
第一,先跑通最小样例。不要一开始就收集几千条轨迹,先拿 20 条左右,把符号化、建树、出图整个链路跑通,确认输出格式自己看得懂,再扩大样本。
第二,输出目录和命名要做好。每次压缩都生成一个带时间戳的转移表 CSV 和自动机图,否则改了几次参数之后,你根本不知道哪张图对应哪组参数。
第三,大部分“行为怪异”问题,先看框架配置,再看模型和提示词。按我的经验,轨迹压缩出来的异常环、异常分支,十次有七八次是框架的循环次数、超时设置、工具调用策略造成的。
智能体轨迹压缩成自动机,说到底不是要把 agent 变成一台机器,而是让你跳出单条日志,从结构上看见 agent 的行为规律。框架决定骨架,模型和提示词决定细节,这个顺序先立住,后续的调试和选型都会省力很多。