跳到主要内容

可观测性与配置:Lakeview 步骤摘要 + 轨迹记录 + 分层配置

30 秒导读: Trae Agent 自称是一个"研究友好"的编码 agent。要研究一个 agent,你得先能看清它每一步在干嘛、能回放它做过的每件事、还能灵活换配置跑对照实验。这一章讲支撑这三点的三块基础设施:Lakeview(把每步概括成一句人话)、TrajectoryRecorder(把每次交互全量写 JSON)、Config(分层优先级的 YAML 配置系统)。

本章是 Trae Agent 系列的收尾章。主循环怎么从提示走到 task_done01-agent-loop.md,多供应商 LLM 抽象见 03-llm-providers.md,工具体系见 02-tools.md。这里只讲"围绕主循环的观测与配置层"。


1. 这是什么(零基础也能懂)

想象你在盯着一个 AI agent 修一个 bug。它跑了 40 步:一会儿读代码、一会儿写测试、一会儿改源码。你面前有三个很实际的痛点:

  • 看不懂:每步的原始输出是一大坨 LLM 文本 + 工具调用参数,扫一眼根本抓不到"这步到底在干嘛"。
  • 没法复盘:跑完了想知道"第 17 步它给模型发了什么、模型回了什么",但屏幕早刷过去了。
  • 换配置麻烦:想做对照实验(换个模型、关掉某个功能),得改一堆地方。

Trae Agent 用三块东西分别解决这三点:

痛点解法核心文件
每步看不懂Lakeview:另开一个 LLM 调用,把每步概括成 <task> 一句话 + <details> 细节 + emoji 标签trae_agent/utils/lake_view.py
没法复盘TrajectoryRecorder:把 messages / response / tool_calls / tool_results 全量落成 JSONtrae_agent/utils/trajectory_recorder.py
换配置麻烦Config:从 YAML 组装,支持"命令行 > 环境变量 > 配置文件 > 默认"的分层覆盖trae_agent/utils/config.py

一句话直觉: Lakeview 是"给人看的字幕",TrajectoryRecorder 是"给机器看的黑匣子录像",Config 是"实验的旋钮面板"。三者都不参与解题本身,但决定了这个 agent 好不好研究。

用起来什么样(Lakeview 输出的字幕效果):

Lakeview Summary
============================================================
[👁️EXAMINE_CODE] The agent is examining source code.
is searching for "create_foo" related to the foo.py line in the stack trace.
[📝WRITE_FIX · 🔥VERIFY_FIX] The agent is fixing the bug and running the test.
edits foo.py to compare sizes correctly, then re-runs test_bug.py.

2. 顶层全景(它大概怎么转)

这三块都挂在主循环旁边。主循环每跑完一步(一个 AgentStep),会同时触发"落轨迹"和"生成字幕";配置则在最开头一次性组装好、注入进去。

怎么读这张图: 中间竖线是主循环的一步;左边是配置在启动时注入,右边是每步向外发出的两条观测支线。

启动时(一次) 每一步(循环里)
┌───────────────────┐
│ Config.create │ 从 YAML 组装
│ (config.py) │ 分层优先级覆盖
└─────────┬─────────┘
│ 注入 model / tools / lakeview

┌───────────────────────────────────────────────┐
│ BaseAgent 主循环 │
│ step = AgentStep(...) │
│ messages = _run_llm_step(step, ...) ────────┼──► LLMClient.chat()
│ _finalize_step(step, ...) │ │
└───────┬──────────────────────────┬────────────┘ │ 每次 LLM 交互
│ │ ▼
▼ ▼ record_llm_interaction()
record_agent_step() create_lakeview_step() │
(给机器:全量 JSON) (给人:字幕摘要) │
│ │ │
▼ ▼ ▼
trajectory_*.json ◄───────────────────────────── 同一份 JSON 文件
{ agent_steps[], llm_interactions[], ... }

部件一句话职责:

部件干什么在哪个文件
Config / Config.create读 YAML,拼出 ModelConfig / TraeAgentConfig / LakeviewConfig 等结构utils/config.py:194,206
resolve_config_value单个值的优先级仲裁:CLI > ENV > 配置文件 > 默认utils/config.py:394
TrajectoryRecorder把每步、每次 LLM 交互全量序列化成 JSON,随写随存盘utils/trajectory_recorder.py:20
LakeView用独立 LLM 把一步概括成 <task>/<details> + 标签utils/lake_view.py:78
discover_mcp_tools连接外部 MCP server,把它们的工具并进 agent 的工具列表agent/trae_agent.py:72

三条主线,后面三节各展开一条。


3. Lakeview:给人看的一句话字幕

3.1 它要解决的小问题

主循环每一步产出的原始内容是"一坨 LLM 文本 + 一串工具调用"。人扫一眼很难判断"这步是在读代码、还是在改代码、还是在跑测试"。Lakeview 的任务:把这一坨压缩成一句 ≤10 词的 task + 一句 ≤30 词的 details,再贴上一个 emoji 标签,让人一眼看懂。

关键设计选择:Lakeview 另起一个独立的 LLM 调用来做概括,而不是让主 agent 顺手总结。这样概括逻辑和解题逻辑解耦,还能用一个更便宜的模型(见 §5 的配置)专门干这件"打字幕"的活。

3.2 思路:两次 LLM 调用,一次抽任务、一次贴标签

Lakeview 对每步做两件事,对应两个精心设计的 prompt:

步骤方法prompt产出
抽任务描述extract_task_in_stepEXTRACTOR_PROMPT<task>…</task><details>…</details>
贴分类标签extract_tag_in_stepTAGGER_PROMPT<tags>WRITE_FIX,VERIFY_FIX</tags>

EXTRACTOR_PROMPT 明确要求两种粒度:<task> 概括且 ≤10 词、去掉任何 bug 特定细节;<details> 补上 bug 特定细节、≤30 词(utils/lake_view.py:16-30)。

TAGGER_PROMPT 给出一张固定的标签清单,让模型从中挑(可多选),清单本身就是"一个修 bug 的 agent 会做哪些动作"的建模(utils/lake_view.py:32-55):

标签含义emoji
WRITE_TEST写/改复现测试脚本☑️
VERIFY_TEST跑复现测试验证环境
EXAMINE_CODE查看/搜索/探索代码👁️
WRITE_FIX改源码修 bug📝
VERIFY_FIX跑测试验证修复🔥
REPORT向用户汇报进展/完成📣
THINK只思考、暂无动作🧠
OUTLIER不属于以上(如装依赖)⁉️

这张 emoji 映射表就是 KNOWN_TAGS(utils/lake_view.py:57-66)。get_label 把标签列表拼成 👁️EXAMINE_CODE · 📝WRITE_FIX 这样的一行(utils/lake_view.py:88-92)。

3.3 一个巧妙点:把答案的开头"喂"给模型

抽任务时,Lakeview 不是干问一句就完事,而是把 assistant 的回复预填了半句,逼模型顺着往下写:

# 示意,非源码:extract_task_in_step 构造的对话
llm_messages = [
user("<previous_step>…</previous_step><this_step>…</this_step>"),
assistant("I understand."),
user(EXTRACTOR_PROMPT),
assistant("Sure. Here is the task the agent is performing: <task>The agent"), # 预填半句
]

真实实现见 utils/lake_view.py:94-106。最后一条 assistant 消息故意停在 <task>The agent —— 模型只能接着补完标签内容,格式因此更稳。extract_tag_in_step 用同样手法预填 <tags>(utils/lake_view.py:154)。

容错细节: 抽任务时如果返回里缺 </task>/<details>/</details> 标签,会重试最多 10 次,还不行就返回空串(utils/lake_view.py:117-130)。贴标签时也重试最多 10 次,且要求解析出的每个标签都在 KNOWN_TAGS 里,否则重来(utils/lake_view.py:158-175)。这两处都把 temperature 强设为 0.1 求稳。

3.4 组装成一步字幕

create_lakeview_step 是入口:把一个 AgentStep 转成文本(_agent_step_str,把 content 和工具调用拼一起,utils/lake_view.py:177-192),然后依次调抽任务和贴标签,打包成 LakeViewStep(desc_task, desc_details, tags_emoji)(utils/lake_view.py:194-209)。

3.5 开关与挂载

Lakeview 是可关的。开关是 TraeAgentConfig.enable_lakeview,默认为 True(utils/config.py:164)。它挂在 CLI console 上而非 agent 核心里——console 每打印完一步,就在后台异步起一个任务生成字幕面板:

# 示意,非源码:simple_console 里的挂载逻辑
if self.lake_view and not step_hist.lake_view_panel_generator:
step_hist.lake_view_panel_generator = asyncio.create_task(
self._create_lakeview_step_display(agent_step) # 后台生成,不阻塞主循环
)

真实实现见 utils/cli/simple_console.py:57-68。跑完后 _print_lakeview_summary 把所有步骤的字幕面板一次性打出来(utils/cli/simple_console.py:110-120)。字幕最终渲染成 [👁️EXAMINE_CODE] The agent <task>\n<details> 这样的 Panel(utils/cli/simple_console.py:222-240)。

注意配置校验: 如果 enable_lakeview 为真但没提供 lakeview 配置,Config.create 会直接报错(utils/config.py:293-294)——因为 Lakeview 需要自己的模型。


4. TrajectoryRecorder:给机器看的黑匣子

4.1 它要解决的小问题

Lakeview 是给人看的摘要,会丢信息。做调试和消融研究(ablation study,改一个变量、跑一遍、看结果差异)时需要的是无损全量:每次给模型发了什么、模型回了什么、调了哪些工具、工具返回了什么。TrajectoryRecorder 就把这些全序列化成一个 JSON 文件。

4.2 JSON 长什么样

一个轨迹文件是一个大字典,骨架在 __init__ 里初始化(utils/trajectory_recorder.py:39-51):

顶层字段内容
task / provider / model / max_steps本次运行的元信息
start_time / end_time / execution_time时间统计
llm_interactions[]每次 LLM 调用的输入消息 + 完整响应 + token 用量
agent_steps[]每步的 messages / response / tool_calls / tool_results / reflection / error
success / final_result最终结果

注意 llm_interactionsagent_steps 是两个不同粒度:一步里可能有多次 LLM 交互,所以两者分开记。

4.3 四个生命周期方法

方法何时调干什么
start_recording任务开始写入 task/provider/model/max_steps,清空两个列表,立即存盘
record_llm_interaction每次 LLM 调用后把 messages + response(含 token 用量、tool_calls)追加进 llm_interactions
record_agent_step每步结束把该步的全部字段追加进 agent_steps
finalize_recording任务结束写入 end_time / success / final_result / execution_time

对应源码:start_recording(trajectory_recorder.py:54)、record_llm_interaction(:77)、record_agent_step(:130)、finalize_recording(:198)。

一个关键设计:随写随存。 上面每个方法末尾都调 save_trajectory() 把整份数据重新 json.dump 到磁盘(trajectory_recorder.py:220-230)。这意味着即使 agent 中途崩了,已跑的步骤也不会丢——CLI 就靠这个打印"Partial trajectory saved"(cli.py:388)。代价是每步都重写整个文件,但对研究场景这份可靠性值得。

4.4 序列化:把富对象拍平成字典

富对象(LLMMessage / ToolCall / ToolResult)靠三个私有辅助拍平:_serialize_message(:232)、_serialize_tool_call(:244)、_serialize_tool_result(:253)。token 用量用 getattr(response.usage, "cache_creation_input_tokens", None) 这种带默认值的方式取,兼容不同供应商 usage 结构的差异(trajectory_recorder.py:106-118)。

4.5 怎么挂到 LLMClient 和 BaseAgent 上

recorder 是同一个实例,同时挂到 agent 和它的 LLM 客户端上——这样两个粒度写进同一个文件。挂载链条:

Agent.__init__
└─ TrajectoryRecorder(trajectory_file) # 建实例 (agent/agent.py:31)


agent.set_trajectory_recorder(recorder) # (agent/agent.py:57)

├─ self._trajectory_recorder = recorder # 挂到 agent
└─ self._llm_client.set_trajectory_recorder(...) # 同一实例也挂到 LLM 客户端
# (base_agent.py:92-96)

两处调用点:

  • 每次 LLM 交互:各供应商客户端在 chat() 拿到响应后调 record_llm_interaction。例如 anthropic_client.py:145if self.trajectory_recorder: self.trajectory_recorder.record_llm_interaction(...);其它供应商(openai / google / ollama / openai_compatible)同样各有一处。
  • 每步结束:BaseAgent._record_handlerrecord_agent_step(base_agent.py:301-312),由 _finalize_step 在每步末尾触发。

任务的开始/结束则由 TraeAgent 负责:new_task 里调 start_recording(agent/trae_agent.py:147-153),execute_task 里调 finalize_recording(agent/trae_agent.py:161-164)。

Lakeview 也能回写轨迹: update_lakeviewstep_number 找到对应步,把 Lakeview 摘要塞进 lakeview_summary 字段并存盘(trajectory_recorder.py:191-196)——于是给人看的字幕也进了黑匣子。


5. 配置系统:分层优先级的 YAML 组装

5.1 它要解决的小问题

做研究要频繁换配置:换模型、换 provider、调 max_steps、开关 Lakeview。Trae Agent 用一套 dataclass 结构 + 一个 Config.create 工厂,从 YAML 一次性组装出全部配置;再用 resolve_config_value 处理"命令行临时覆盖"这类需求。

5.2 配置的结构(dataclass 层级)

Config (config.py:194)
├─ model_providers: {name → ModelProvider} # api_key / provider / base_url / api_version
├─ models: {name → ModelConfig} # model / temperature / max_tokens / ...
├─ lakeview: LakeviewConfig # 只含一个 model: ModelConfig
└─ trae_agent: TraeAgentConfig # max_steps / tools / enable_lakeview
└─ (继承 AgentConfig) mcp_servers_config / allow_mcp_servers / model
  • ModelProvider(config.py:16):一个供应商的 api_key、base_url 等。
  • ModelConfig(config.py:29):一个具体模型的采样参数,持有一个 ModelProvider 引用。
  • MCPServerConfig(config.py:119):一个 MCP server 的连接方式(command/args/url/http_url 等)。
  • TraeAgentConfig(config.py:158):agent 本身的配置,enable_lakeview 默认 Truetools 默认那四个内置工具。
  • LakeviewConfig(config.py:184):只包一个 model,即"打字幕用哪个模型"。

5.3 Config.create:从 YAML 组装

Config.create(config.py:206)是唯一的组装入口。它读一段 YAML(从文件或字符串),按固定顺序拼装:

1. 若文件以 .json 结尾 → 转走 create_from_legacy_config (config.py:218-219)
2. 解析 model_providers → {name: ModelProvider} (config.py:232-239)
3. 解析 models → {name: ModelConfig},并把每个 (config.py:242-254)
model 的 model_provider 字符串替换成真正的 ModelProvider 对象
4. 解析 lakeview → 按名字从 models 里取那个模型 (config.py:257-267)
5. 解析 mcp_servers / allow_mcp_servers (config.py:269-272)
6. 解析 agents.trae_agent → TraeAgentConfig,校验 lakeview (config.py:275-299)

设计要点:models 引用 providers 是先存字符串名、组装时再解引用成对象(config.py:246-251)。任何一步缺东西都抛 ConfigError(比如没 model_providers、model 引用了不存在的 provider),让配置错误尽早暴露而不是跑到一半才崩。

一份最小 YAML 长这样(节选自 trae_config.yaml.example):

agents:
trae_agent:
enable_lakeview: true
model: trae_agent_model # 指向下面 models 里的名字
max_steps: 200
lakeview:
model: lakeview_model # 字幕用一个更便宜的模型
models:
trae_agent_model:
model_provider: anthropic # 指向下面 model_providers 里的名字
model: claude-4-sonnet
lakeview_model:
model_provider: anthropic
model: claude-3.5-sonnet
model_providers:
anthropic:
api_key: your_anthropic_api_key
provider: anthropic

注意 lakeview 特意指向一个更便宜的模型(claude-3.5-sonnet),而主 agent 用 claude-4-sonnet——这正是 §3.1 说的"打字幕用便宜模型"。

5.4 resolve_config_value:分层优先级仲裁

resolve_config_value(config.py:394-410)是个只有几行的纯函数,却是整个"分层配置"的心脏。它对单个值按固定优先级挑第一个非空的:

# 真实源码:config.py:401-410
def resolve_config_value(*, cli_value, config_value, env_var=None):
if cli_value is not None: # ① 命令行最高
return cli_value
if env_var and os.getenv(env_var): # ② 环境变量次之
return os.getenv(env_var)
if config_value is not None: # ③ 配置文件再次
return config_value
return None # ④ 都没有 → 交给调用方给默认

优先级链:命令行 > 环境变量 > 配置文件 > 默认

谁在用它?ModelConfig.resolve_config_values(config.py:66)用它解析 model 名、api_key、base_url——注意 api_key 的环境变量名是按 provider 动态拼的:ANTHROPIC_API_KEYOPENAI_BASE_URL 这类(config.py:97-98)。TraeAgentConfig.resolve_config_values(config.py:174)用它解析 max_steps。顶层 Config.resolve_config_values(config.py:302)把这些串起来,在 CLI 解析完命令行参数后调一次。

这套设计让研究者可以:配置文件放稳定的基线,环境变量放密钥,命令行放"这次实验临时想改的那个旋钮"。

5.5 create_from_legacy_config:兼容旧 JSON

早期 Trae Agent 用的是扁平的 trae_config.json。为了不破坏老用户,create_from_legacy_config(config.py:324)把旧 JSON 翻译成新结构:.json 结尾的配置文件会自动走这条路(config.py:218-219)。

旧格式由 legacy_config.pyLegacyConfig 解析(legacy_config.py:71),它是扁平的 default_provider + model_providers + max_steps。翻译逻辑:取 default_provider 对应那一项,拼成一个 ModelProvider + 一个名为 default_modelModelConfig,再包成新 Config(config.py:339-391)。旧格式里 model 参数和 provider 参数是揉在一起的(ModelParameters 同时含 api_key 和采样参数,legacy_config.py:19-33),新格式把它们拆成两层(provider 与 model 分离)——这正是新 schema 想改进的地方。


6. 顺带:MCP 工具发现如何并入工具列表

配置系统里的 MCPServerConfig / allow_mcp_servers 有个下游用途:把外部 MCP server 的工具并进 agent 的工具列表。MCP(Model Context Protocol)是一个让 agent 连外部工具服务的协议。

流程由 TraeAgent.discover_mcp_tools(agent/trae_agent.py:72)驱动:

discover_mcp_tools()
遍历 mcp_servers_config 里的每个 server
│ 跳过不在 allow_mcp_servers 白名单里的 (trae_agent.py:77)

MCPClient().connect_and_discover(...) (mcp_client.py:39)
│ 按 stdio 启动 server(command/args/env/cwd)
│ session.list_tools() 拿到 server 提供的工具

每个工具包成 MCPTool,append 进 mcp_tools 容器 (mcp_client.py:68-70)

发现完后 initialise_mcpself.mcp_tools 追加进 agent 的 _tools(agent/trae_agent.py:65-70),外部工具于是和内置工具(bash、编辑器等,见 02-tools.md)平等地进入主循环。失败的 server 会被静默跳过并清理(agent/trae_agent.py:89-98),不阻塞整体运行。

注意当前边界: connect_and_discover 里只实现了 stdio 传输;http_urlurl(WebSocket)分支直接抛 NotImplementedError(mcp_client.py:47-50)。


7. 巧妙之处(可借鉴的技术)

  • 观测与解题解耦。 Lakeview 用独立 LLM 调用打字幕,还能配独立的更便宜模型(config.py:184LakeviewConfig 单独持有 model)。研究者要的"可读性"不该污染 agent 的解题上下文,也不该按主模型的价格付费。
  • 预填 assistant 半句逼出结构。 extract_task_in_step 把最后一条 assistant 消息停在 <task>The agent(lake_view.py:104),extract_tag_in_step 停在 <tags>(lake_view.py:154)。这是一种轻量的"约束解码":不改采样、只靠对话构造,就把输出格式钉住,再配重试兜底。
  • 随写随存的黑匣子。 每次记录都 save_trajectory() 全量落盘(trajectory_recorder.py 各方法末尾),换来"崩了也有部分轨迹"。对做实验的人,一次崩溃的部分数据往往比什么都没有强得多。
  • 一个 recorder 双粒度挂载。 同一个 TrajectoryRecorder 既挂 agent(记步)又挂 LLM 客户端(记每次交互),两个粒度进同一文件(base_agent.py:92-96),复盘时步和交互能对齐。
  • 配置错误尽早炸。 Config.create 对每个缺失项都抛 ConfigError(缺 provider、model 引用错、Lakeview 开着却没配)。研究场景最怕"跑了半小时才发现配置写错",这套前置校验把失败提前到启动瞬间。
  • 分层优先级只用一个纯函数实现。 resolve_config_value 十行不到(config.py:394-410),却统一了 CLI / ENV / 文件 / 默认四层的仲裁逻辑,处处复用、无隐藏状态。

8. 边界与局限(诚实)

  • 每步全量重写文件。 save_trajectory 每次都把整份数据 json.dump 一遍(trajectory_recorder.py:220-230)。步数极多、轨迹极大时会有 IO 开销;换来的是可靠性,不是性能。
  • Lakeview 的 prompt 是"修 bug"专用的。 EXTRACTOR_PROMPT / TAGGER_PROMPT 里全是 SWE 语境("solve a software bug"),标签清单也是围绕修 bug 设计(lake_view.py:16-55)。用在非修 bug 的任务上,字幕质量会打折。
  • Lakeview 有额外 LLM 成本。 每步至少两次额外 LLM 调用(抽任务 + 贴标签),失败还会重试最多 10 次(lake_view.py:117-130,158-175)。这是"可读性"的价钱。
  • 贴标签会因过长而放弃。 extract_tag_in_step 里若历史步骤拼起来超过 300000 字符就直接返回空标签、不打标(lake_view.py:143-145)。长任务后段会丢标签。
  • MCP 只支持 stdio。 HTTP / WebSocket 传输未实现(mcp_client.py:47-50)。
  • legacy 配置只取默认 provider。 create_from_legacy_config 只翻译 default_provider 那一项、生成单个 default_model(config.py:339-391),旧 JSON 里配的其它 provider 不会被带过来。

9. 横向对比(同 shelf 兄弟章)

这一章是 Trae Agent"研究友好"定位的落点:它不改变 agent 怎么解题(01-agent-loop.md),而是在旁边加了三层观测与配置。

  • 03-llm-providers.md:TrajectoryRecorder 的 record_llm_interaction 挂在每个供应商客户端的 chat() 里,是统一 LLM 抽象的"旁路记录点"。
  • 02-tools.md:MCP 工具发现把外部工具并进同一个工具注册流程,和内置工具平权。
  • 05-docker-execution.md:两者都是"包在主循环外的横切设施"——一个管安全边界,一个管观测与配置。

10. 代码地图(导航索引)

主题文件路径符号名
Lakeview 主类trae_agent/utils/lake_view.pyLakeView
组装一步字幕trae_agent/utils/lake_view.pyLakeView.create_lakeview_step
抽任务描述trae_agent/utils/lake_view.pyLakeView.extract_task_in_step / EXTRACTOR_PROMPT
贴分类标签trae_agent/utils/lake_view.pyLakeView.extract_tag_in_step / TAGGER_PROMPT / KNOWN_TAGS
字幕面板渲染trae_agent/utils/cli/simple_console.pySimpleCLIConsole._create_lakeview_step_display
轨迹记录器trae_agent/utils/trajectory_recorder.pyTrajectoryRecorder
记一步trae_agent/utils/trajectory_recorder.pyTrajectoryRecorder.record_agent_step
记一次 LLM 交互trae_agent/utils/trajectory_recorder.pyTrajectoryRecorder.record_llm_interaction
开始/结束记录trae_agent/utils/trajectory_recorder.pystart_recording / finalize_recording
recorder 双粒度挂载trae_agent/agent/base_agent.pyBaseAgent.set_trajectory_recorder / _record_handler
配置组装工厂trae_agent/utils/config.pyConfig.create
分层优先级仲裁trae_agent/utils/config.pyresolve_config_value
旧 JSON 兼容trae_agent/utils/config.pyConfig.create_from_legacy_config
配置结构trae_agent/utils/config.pyModelConfig / ModelProvider / TraeAgentConfig / LakeviewConfig / MCPServerConfig
旧配置解析trae_agent/utils/legacy_config.pyLegacyConfig
MCP 工具发现trae_agent/agent/trae_agent.pyTraeAgent.discover_mcp_tools
MCP 连接与列工具trae_agent/utils/mcp_client.pyMCPClient.connect_and_discover