数据截至 (上游 commit e839e559ac61)
核心主循环:一次任务如何从提示走到 task_done
30 秒导读: Trae Agent 的心脏是一个
while循环。它把「系统提示 + 你的问题」丢给 LLM, LLM 要么说"我要调这几个工具",要么调一个特殊的task_done工具宣布收工。循环每一轮就是 "问模型 → 跑它要的工具 → 把结果塞回对话",直到模型喊停(且改动非空)或步数用尽。本章只讲这条 主线;工具怎么实现看 第 2 章,LLM 供应商细节看 第 3 章。
1. 这是什么(零基础也能懂)
一句话定义: 一个"中央循环",它不停地和大模型对话,把大模型的每一句"我想干什么"翻译成 真实的动作(读文件、跑 bash、改代码),再把动作结果讲给大模型听,直到任务办完。
它解决什么问题: 大模型本身只会"输出文字"。要让它真的去修一个 GitHub issue——读代码、 改文件、跑测试——就得有人在旁边反复地:接住它的意图、执行、把结果喂回去。这个"旁边的人" 就是 agent 主循环。
用起来什么样: 你给它一个仓库路径和一段问题描述,它自己转起来:
[system] 你是一个软件工程 agent……(TRAE_AGENT_SYSTEM_PROMPT)
[user] [Project root path]: /home/me/proj
[Problem statement]: 函数 foo 在空输入时崩溃……
step 1 → LLM: "我先看看 foo 在哪" → 调 bash: grep -rn "def foo"
step 2 → LLM: "读一下这个文件" → 调 str_replace_based_edit_tool(view)
step 3 → LLM: "改这一行" → 调 str_replace_based_edit_tool(edit)
step 4 → LLM: "跑一下测试" → 调 bash: pytest
step 5 → LLM: "修好了" → 调 task_done ✅ 收工
一句话直觉: 把它想成一个边想边动手的实习生 + 一个死心眼的调度员。实习生(LLM)每轮
说一句"下一步干嘛",调度员(主循环)负责真去干、把结果原样汇报回来,并且只认一个暗号
(task_done)才让实习生下班。
2. 顶层全景(它大概怎么转)
主循环住在 BaseAgent.execute_task(trae_agent/agent/base_agent.py:147)。一轮迭代("一步")
拆成三个动作:问 LLM → 处理它的回复 → 记账。
2.1 这张图怎么读
从上到下是一步(step)的生命周期;右侧两个分叉是 LLM 回复的两种可能——"要收工"或"要用工具"。 命中收工分叉且校验通过就跳出循环,否则回到顶部进入下一步。
┌──────────────────────────────────────────────┐
│ while step_number <= max_steps │
│ (execute_task, base_agent.py:163) │
└────────────── ─────┬───────────────────────────┘
│ 每轮新建一个 AgentStep(THINKING)
▼
┌─────────────────────┐
│ _run_llm_step │ ← 问模型
│ llm_client.chat() │
└──────────┬──────────┘
│ LLM 回复
┌─────────────┴──────────────┐
▼ ▼
┌────────────────────┐ ┌──────────────────────────┐
│ 说"我完成了"? │ 否 │ _tool_call_handler │
│ (task_done 调用) │─────▶ │ 跑工具 → ToolResult │
└─────────┬──────────┘ │ 转成 role=user 消息回填 │
│ 是 └────────────┬─────────────┘
▼ │
┌────────────────────┐ │
│ 改动非空? │ 否:回一句 │
│ (must_patch 校验) │─── task_incomplete ─┘
└─────────┬──────────┘ 继续下一步
│ 是
▼
COMPLETED ── break ── 记轨迹 / 收尾
2.2 部件一句话职责
| 部件 | 干什么 | 在哪 |
|---|---|---|
execute_task | 主循环本体:管步数、管状态、异常兜底 | base_agent.py:147 |
_run_llm_step | 一步的核心:调 LLM,判完成还是走工具 | base_agent.py:209 |
_tool_call_handler | 把 tool_calls 交给执行器,把结果转成消息 | base_agent.py:314 |
_finalize_step | 收尾一步:标 COMPLETED、记轨迹、更新界面 | base_agent.py:238 |
AgentStep / AgentState | 状态机:记录"这一步/整个任务"处在什么阶段 | agent/agent_basics.py:19 |
new_task | 循环开跑前,拼好 system + user 初始消息 | trae_agent.py:102 |
llm_indicates_task_completed + _is_task_completed | 双重完成判定(暗号 + 改动校验) | trae_agent.py:228/236 |
2.3 主线走一遍(不进代码)
- 组装任务:
TraeAgent.new_task把系统提示和[Project root path] / [Problem statement]拼成_initial_messages(见 §5)。 - 进循环:
execute_task从step_number = 1开始,只要没超max_steps就一轮轮转。 - 每轮:
_run_llm_step问模型;模型没喊停就_tool_call_handler跑工具、把结果回填成 下一轮的输入消息 。 - 收工:模型调
task_done且改动非空 → 状态置COMPLETED→break。 - 兜底:任何一步抛异常 → 状态
ERROR、break;步数耗尽还没成功 → 也置ERROR。
3. 核心机制(逐个拆)
3.1 状态机:AgentStep / AgentStepState / AgentState
要解决的小问题: 循环转很多轮,每轮内部又分"在想 / 在调工具 / 在反思"几个阶段。得有个 统一的词汇把"现在到哪了"记下来——既给界面显示,也给轨迹记录。
两层状态,别混:
| 类型 | 粒度 | 取值 | 出处 |
|---|---|---|---|
AgentState | 整个任务 | IDLE / RUNNING / COMPLETED / ERROR | agent_basics.py:29 |
AgentStepState | 单独一步 | THINKING / CALLING_TOOL / REFLECTING / COMPLETED / ERROR | agent_basics.py:19 |
AgentStep(agent_basics.py:38)是"一步的记事本":它带 step_number、当前 state、这一步的
llm_response、tool_calls、tool_results、reflection、error。循环每轮 new 一个新的
AgentStep,初始状态 THINKING:
# base_agent.py:164 —— 每轮开头新建一步,状态 THINKING
step = AgentStep(step_number=step_number, state=AgentStepState.THINKING)
AgentExecution(agent_basics.py:66)则是"整场任务的账本":task、所有 steps、
final_result、success、total_tokens、agent_state。循环开始前 agent_state 置
RUNNING(base_agent.py:161),结束时是 COMPLETED 或 ERROR。
关键点——success 和状态是两回事: 只有走到"完成判定"全过了才会 execution.success = True
(base_agent.py:229)。步数耗尽时代码专门检查 not execution.success 才判 ERROR
(base_agent.py:179),避免把"最后一步刚好完成"误判成超时。
3.2 一步的核心:_run_llm_step
要解决的小问题: 一步里到底发生什么?答:问一次模型,然后二选一——要么模型宣布完成, 要么模型要用工具。
看它的骨架(base_agent.py:209):
# base_agent.py:216 —— 问模型(工具清单一起传进去,让模型知道能调什么)
llm_response = self._llm_client.chat(messages, self._model_config, self._tools)
step.llm_response = llm_response
self._update_llm_usage(llm_response, execution) # 累加 token 用量
if self.llm_indicates_task_completed(llm_response): # 模型喊停了?
if self._is_task_completed(llm_response): # 且改动非空?
execution.agent_state = AgentState.COMPLETED
execution.final_result = llm_response.content
execution.success = True
return messages
else: # 喊停了但 patch 是空的
execution.agent_state = AgentState.RUNNING
return [LLMMessage(role="user", content=self.task_incomplete_message())]
else:
tool_calls = llm_response.tool_calls
return await self._tool_call_handler(tool_calls, step) # 去跑工具
注意这个方法的返回值是"下一轮要喂给 LLM 的 messages"。完成时返回原样(循环随后会 break); 未完成时返回"工具结果消息"或"任务未完成的提醒"。主循环靠这条把上一轮的产出接到下一轮的输入:
# base_agent.py:166 —— 上一步的返回值,就是下一步 chat() 的输入
messages = await self._run_llm_step(step, messages, execution)
chat() 本身如何跨供应商、如何重试,是 第 3 章 的事,这里只当它是个
"给消息返回回复"的黑盒。
3.3 跑工具并回填:_tool_call_handler
要解决的小问题: 模型说"调 bash 跑 pytest",这只是一段结构化意图。得真去执行,再把
输出(stdout / 报错)变成模型下一轮能读到的对话消息。
流程(base_agent.py:314):
- 空调用兜底:如果模型既没喊停、又没给任何
tool_calls,回一句"看起来你还没完成任务", 让它下一轮继续(base_agent.py:318)。 - 交给执行器:按配置选并行或串行执行——
parallel_tool_call或sequential_tool_call(base_agent.py:331)。执行器实现见 第 2 章。 - 回填结果:每个
ToolResult包成一条role="user"的LLMMessage——注意用的是user角色 +tool_result字段,而不是文本:
# base_agent.py:337 —— 工具结果以 user 消息回填,交给供应商层去翻译成各家的 tool 格式
for tool_result in tool_results:
message = LLMMessage(role="user", tool_result=tool_result)
messages.append(message)
这条 LLMMessage 只是统一的中间表示;各家 API(OpenAI 的 tool 角色、Anthropic 的
tool_result block)由供应商层翻译,见 第 3 章。
期间 step.state 依次走 CALLING_TOOL(base_agent.py:327),必要时 REFLECTING(见 §3.4)。
3.4 反思:reflect_on_result —— 基类拼失败,TraeAgent 关掉它
要解决的小问题: 工具报错了,要不要主动提醒模型"这步失败了,换个法子"?
基类的做法(base_agent.py:246):把所有失败的工具结果拼成一段反思文字,追加为一条
role="assistant" 消息,并把这一步标成 REFLECTING:
# base_agent.py:251 —— 基类:把失败工具拼成一句"失败了,换个思路"的反思
reflection = "\n".join(
f"The tool execution failed with error: {tool_result.error}. "
f"Consider trying a different approach or fixing the parameters."
for tool_result in tool_results
if not tool_result.success
)
TraeAgent 的做法:直接覆盖成 return None(trae_agent.py:176)——关掉自动反思。
为什么?因为工具错误信息本身已经作为 tool_result 回填给模型了(§3.3);再额外塞一段
assistant 独白反而多余。TraeAgent 信任模型自己看到报错就会调整。所以在 TraeAgent 里,
_tool_call_handler 里那段 if reflection: 分支(base_agent.py:342)永远不进,step.state
也就不会变成 REFLECTING。
3.5 完成判定:两道关卡
要解决的小问题: 什么才算"任务办完了"?这是最容易踩坑的地方——不能光凭模型嘴上说完成。
TraeAgent 设了两道关,_run_llm_step 里先后调用(§3.2)。
第一关 · 模型是否喊停? llm_indicates_task_completed
| 基类 | TraeAgent(覆盖) | |
|---|---|---|
| 判定方式 | 关键词匹配 | 检测工具调用 |
| 逻辑 | content 里含 "done" / "task completed" 等词 | 是否调了名为 task_done 的工具 |
| 出处 | base_agent.py:259 | trae_agent.py:228 |
基类靠模糊的关键词("done" 之类)容易误判——模型随口说个 "done" 就被当成完成。TraeAgent 改成
显式暗号:模型必须真的发起一个 task_done 工具调用,才算喊停:
# trae_agent.py:231 —— TraeAgent:只认 task_done 工具调用,不认关键词
if llm_response.tool_calls is None:
return False
return any(tool_call.name == "task_done" for tool_call in llm_response.tool_calls)
task_done 是注册在 TraeAgentToolNames 里的内置工具(trae_agent.py:21);系统提示结尾也明确
要求"确定解决了就调 task_done"(prompt/agent_prompt.py:52)。
第二关 · 改动是否非空? _is_task_completed 的 must_patch 校验
模型喊停不代表真改了东西——它可能空手宣布完成。当 must_patch == "true" 时(常用于 SWE-bench
这类评测),TraeAgent 会实际检查有没有产生代码改动(trae_agent.py:236):
# trae_agent.py:238 —— 第二关:must_patch 模式下,空 patch 不算完成
if self.must_patch == "true":
model_patch = self.get_git_diff() # git diff 拿改动
patch = self.remove_patches_to_tests(model_patch) # 剔掉对测试目录的改动
if not patch.strip(): # 剔完是空的 → 不算完成
return False
return True
get_git_diff(trae_agent.py:180):cd到项目目录跑git --no-pager diff(有base_commit时跑diff base_commit HEAD),完了cd回原目录。remove_patches_to_tests(trae_agent.py:205,源自 aider-swe-bench):把改到测试目录 (/test/、/tests/、test_、tox.ini等)的 hunk 过滤掉——因为改测试不算"修 bug",只有 改产品代码才算数。
没过第二关会怎样? _run_llm_step 不 break,而是把状态退回 RUNNING,并返回一条催促消息
(base_agent.py:232):
# trae_agent.py:247 —— 空 patch 时回给模型的话
return "ERROR! Your Patch is empty. Please provide a patch that fixes the problem."
模型收到这句后会继续下一步,直到真拿出改动。这套"关键词 → 显式暗号 → 实打实校验改动"的收紧, 是 TraeAgent 相比基类最关键的工程加固。