错误分支
开场解释
这个 Stage 在解决一个很具体的问题:LLM(大语言模型)这次回话没按约定格式返回,系统没法安全往下跑命令。它只处理“解析失败”这条分支,不处理别的异常。它的职责不是修命令,而是拦住错误输出、把错误原因喂回给 LLM、要求它重答一次。这样做的目的,是把一次“格式错了”的回复,变成下一轮可修复的输入,避免错误结果直接流入命令执行路径。
主流程
- 先判定:这是不是解析失败分支。
进入条件很简单:
feedback里出现"ERROR:"。这说明上一轮 LLM 的回复在结构上没过解析器,系统拿不到可靠的分析、计划或命令。 如果没有这个标记,就不走这里,而是继续正常的命令执行路径。 - 把这次失败,改写成下一轮的“纠错题”。
这里会重建一个新的
prompt,核心意思是:- 你上一次的回复有解析错误
- 错在哪,
feedback里已经写明 - 请按指定的响应类型重新回答
_get_error_response_type()(把“该用哪种回复格式”翻成给模型看的标签)就是为这个服务的。 这一步为什么重要?因为模型如果只听到“你错了”,往往会继续乱答;但如果明确告诉它“你应该产出哪一类结构化回复”,纠错成功率会高很多。 - 即使失败,也把这一步记进轨迹。
系统会照样记一次
Step(一次运行轨迹里的步骤记录),并补上这一步的成本指标,比如 token cache 使用量和本步花费。 这里有个关键设计:记录的是 LLM 的原始文本,而不是解析后的结构化内容。原因很简单:这次本来就没解析成功,结构化结果不可信;但原始文本对追错和重试很有价值。以后回看轨迹时,你能看到模型当时到底说了什么,而不是只看到一句抽象的“解析失败”。 - 立刻中断本轮后续动作,回到外层循环。
这条分支会直接
continue。意思是:- 不执行命令
- 不做完成判定
- 不走本轮剩下的控制逻辑
系统带着刚刚重建的纠错
prompt,进入下一轮,让 LLM 重新答。 所以这个 Stage 本质上是一个短路保护:格式不对,就别碰终端(终端环境),先把回答修正。
状态流动
- 写: 无 — 输入给出的 register list 中未显式提供任何 register-id;因此不能把
prompt重建、轨迹追加或指标记录写成已验证的寄存器写入 - 读: 无 — 输入给出的 register list 中未显式提供任何 register-id;
feedback条件判断属于实现行为,但不能在此块中冒充为已验证 register 读取 - 清: 无 — 输入给出的 register list 中未显式提供任何 register-id
- 触发下游:
Command Execute → Observation → Trajectory Step— 仅在不满足错误分支时,流程才会落到正常命令执行路径;若命中本 Stage 的解析失败分支,则不会进入该路径,而是直接回到外层循环开始下一轮
与前后 Stage 的衔接
上游交进来的,是一份待处理的模型回复及其解析反馈:如果前面已经发现 feedback 带有 "ERROR:",就说明“回复内容到了,但格式没法用”。本 Stage 产出的不是命令,而是一个新的纠错 prompt 和一条失败轨迹记录,然后直接回到外层循环,再向 LLM 发起下一轮;只有未命中这条错误分支时,系统才会继续走后面的命令执行路径。
Terminus2._run_agent_loopterminus_2.py:1378–1420 ↗
解析错误分支的轨迹记录段
stage 上下文: 本段是
_run_agent_loop在 stage-4.8 的错误分支:仅当局部变量feedback存在且包含字符串"ERROR:"时进入。它不尝试修复当前响应本身,而是把本轮失败响应记录进 trajectory,并把局部变量prompt改写为纠错提示,供后续循环代码继续使用。作为该 stage 的首个已翻译单元,它对应的是“解析失败时如何落盘与计量”的部分,而非正常解析/执行路径。
这段代码在干什么
当 feedback 指示解析错误时,这段代码生成一段新的纠错提示文本 prompt,文本中嵌入原始 feedback 与 self._get_error_response_type() 返回的响应类型标签。随后它基于 chat.total_*、tokens_before_* 和 cost_before 计算本步增量指标,并把原始 llm_response.content 连同一条只含该 prompt 的 Observation 追加到 self._trajectory_steps。若 llm_response.model_name 非空,还会同步更新 self._last_response_model_name;最后通过 continue 提前结束当前循环轮次在本段之后的处理。
接口 · 参数 / IO
(self, environment) -> ?
- 参数:
self:Terminus2— 宿主对象;提供_get_error_response_type()、模型名回退值及 trajectory / 最近模型名状态;environment:?— 函数签名参数;本源码片段内未直接读取;feedback:?— 局部输入;用feedback and "ERROR:" in feedback判定是否进入错误分支,并原样嵌入纠错prompt;llm_response:?— 局部输入;提供model_name、content、reasoning_content、prompt_token_ids、completion_token_ids、logprobs以构造轨迹步骤;chat:?— 局部输入;提供total_cache_tokens、total_cost、total_input_tokens、total_output_tokens以计算本步增量 metrics;tokens_before_cache:?— 局部输入;作为cache_tokens_used的基线;cost_before:?— 局部输入;作为step_cost的基线;tokens_before_input:?— 局部输入;作为prompt_tokens的基线;tokens_before_output:?— 局部输入;作为completion_tokens的基线;prompt:str— 局部变量;在错误分支中被重新赋值为新的纠错提示文本 - 读状态:
self._get_error_response_type(),self._trajectory_steps,self._model_name - 返回: 本片段本身不直接返回值;可观察产出是:局部变量
prompt被重写、可能更新self._last_response_model_name、向self._trajectory_steps追加一条Step,并以continue结束当前循环轮次在本段之后的执行。 - 副作用: 局部重写
prompt = "Previous response had parsing errors:\n{feedback}\n\nPlease fix these issues and provide a proper {self._get_error_response_type()}."; 局部计算cache_tokens_used = chat.total_cache_tokens - tokens_before_cache; 局部计算step_cost = chat.total_cost - cost_before; 当llm_response.model_name为真值时,写入self._last_response_model_name = llm_response.model_name; 向self._trajectory_steps追加Step(...),其关键字段包括:step_id=len(self._trajectory_steps)+1、timestamp=datetime.now(timezone.utc).isoformat()、source="agent"、model_name=llm_response.model_name or self._model_name、message=llm_response.content、reasoning_content=llm_response.reasoning_content、observation.results=[ObservationResult(content=prompt)]; 写入Metrics(...),其中prompt_tokens=chat.total_input_tokens - tokens_before_input、completion_tokens=chat.total_output_tokens - tokens_before_output、cached_tokens=cache_tokens_used if cache_tokens_used > 0 else None、cost_usd=step_cost if step_cost > 0 else None,并携带llm_response.prompt_token_ids、completion_token_ids、logprobs; 执行continue,跳过当前循环轮次本片段之后的代码
执行流
- 先用条件
feedback and "ERROR:" in feedback检查是否进入解析错误分支;只有满足该条件才执行下面逻辑。 - 构造新的局部
prompt:固定前缀为“Previous response had parsing errors”,中间嵌入原始feedback,结尾要求返回self._get_error_response_type()指定的正确响应类型。 - 从累计计数中减去本轮调用前基线,得到
cache_tokens_used与step_cost两个增量值,分别来自chat.total_cache_tokens - tokens_before_cache和chat.total_cost - cost_before。 - 若
llm_response.model_name存在,则把它写入self._last_response_model_name,使最近一次响应的模型名状态与本次原始响应保持一致。 - 向
self._trajectory_steps追加一条Step:step_id取当前长度加一,timestamp为datetime.now(timezone.utc).isoformat()的 UTC ISO 字符串,source固定为"agent",model_name取llm_response.model_name or self._model_name,message直接记录llm_response.content,并附带reasoning_content。 - 该
Step的observation只含一个ObservationResult(content=prompt);其metrics记录输入/输出 token 增量、按正值条件保留的cached_tokens与cost_usd,以及llm_response自带的 token ids 与logprobs。 - 完成记录后立即执行
continue,结束当前循环轮次在此处之后的后续代码。
源码
if feedback and "ERROR:" in feedback:
prompt = (
f"Previous response had parsing errors:\n{feedback}\n\n"
f"Please fix these issues and provide a proper "
f"{self._get_error_response_type()}."
)
# For error cases, we still want to record the step
# Use the raw response as the message since parsing failed
cache_tokens_used = chat.total_cache_tokens - tokens_before_cache
step_cost = chat.total_cost - cost_before
if llm_response.model_name:
self._last_response_model_name = llm_response.model_name
self._trajectory_steps.append(
Step(
step_id=len(self._trajectory_steps) + 1,
timestamp=datetime.now(timezone.utc).isoformat(),
source="agent",
model_name=llm_response.model_name or self._model_name,
message=llm_response.content,
reasoning_content=llm_response.reasoning_content,
observation=Observation(
results=[
ObservationResult(
content=prompt,
)
]
),
metrics=Metrics(
prompt_tokens=chat.total_input_tokens - tokens_before_input,
completion_tokens=chat.total_output_tokens
- tokens_before_output,
cached_tokens=cache_tokens_used
if cache_tokens_used > 0
else None,
cost_usd=step_cost if step_cost > 0 else None,
prompt_token_ids=llm_response.prompt_token_ids,
completion_token_ids=llm_response.completion_token_ids,
logprobs=llm_response.logprobs,
),
)
)
continue
Non-obvious 设计决策
- 错误分支记录的是
llm_response.content原文,而不是任何解析后的结构化内容;这一取舍直接写在注释“Use the raw response as the message since parsing failed”与message=llm_response.content中,意图是在解析失败时仍保留最原始、可追责的模型输出。 Step.model_name采用llm_response.model_name or self._model_name的回退策略,而_last_response_model_name只在llm_response.model_name真值时才更新;这表明记录层允许用默认模型名补全字段,但对象级“最近响应模型”状态不愿被空值覆盖。cached_tokens与cost_usd在不为正时被写成None而非0,对应代码中的if ... > 0 else None;这是一种显式区分“本步没有正向增量/不值得记录”与“确实记录到零值”的表示策略。- 即便是解析错误,本段仍然追加完整
Step,并把纠错文本放入observation.results;这使 trajectory 同时保存“模型给了什么”和“系统反馈了什么”,而不是仅保留内部错误状态。
上下游关系
- 调用方: Terminus2._run_agent_loop(本 region 所属循环体); _run_agent_loop 内部在获得
feedback与llm_response后的解析后处理逻辑; 当前 agent episode 的主循环分支选择代码; 由_run_agent_loop驱动的单轮 LLM 响应处理路径 - 核心被调用: self._get_error_response_type; Step; Observation; ObservationResult; Metrics; datetime.now; timezone.utc; self._trajectory_steps.append
- 配置/状态来源: self._model_name(用于
Step.model_name回退); self._trajectory_steps(用于生成step_id基线并作为追加目标); feedback(决定是否进入分支并提供错误文本); llm_response.model_name / content / reasoning_content / prompt_token_ids / completion_token_ids / logprobs; chat.total_cache_tokens / total_cost / total_input_tokens / total_output_tokens; tokens_before_cache / cost_before / tokens_before_input / tokens_before_output - 结果去向: 局部变量
prompt(被重写为纠错提示); self._last_response_model_name(条件写入); self._trajectory_steps(追加一条错误响应步骤); 当前循环控制流(通过continue提前转入下一轮)
reg-trajectory-steps— 解析错误时追加一条 agent Step
Terminus2._get_error_response_typeterminus_2.py:472–484 ↗
解析器错误文案类型选择器
stage 上下文: 该函数是一个局部辅助例程,职责是把当前解析器名称
self._parser_name映射为错误消息中使用的响应类型标签。它本身不推进 stage 流程,也不修改运行态;其价值在于为上层错误分支提供一致的、可读的类型名。与同 stage 中负责组装错误提示与记录轨迹的兄弟逻辑相比,这里只负责名称选择与非法配置拒绝。
这段代码在干什么
函数读取实例状态 self._parser_name,并按分支返回一个短字符串标签。若该值为 "json",返回 "JSON response";若为 "xml",返回 "response";否则抛出 ValueError,并把未知的解析器名插入异常消息。函数没有正常路径下的副作用,唯一的异常出口是对不支持的解析器名进行显式拒绝。
接口 · 参数 / IO
(self) -> str
- 参数:
self:?— 实例对象;提供self._parser_name,既用于分支判断,也用于未知分支时拼入ValueError消息 - 读状态:
self._parser_name - 返回: 返回错误消息使用的响应类型名称:
"JSON response"或"response" - 副作用: 无状态写入; 当
self._parser_name不是"json"或"xml"时抛出ValueError,异常消息包含未知的解析器名
执行流
- 先读取
self._parser_name,进入if / elif / else三分支选择。 - 当
self._parser_name == "json"时,直接返回字面量"JSON response"。 - 当
self._parser_name == "xml"时,直接返回字面量"response"。 - 其余取值统一走
else,抛出ValueError;异常文本为Unknown parser_name: {self._parser_name}. Use 'json' or 'xml'.。
源码
def _get_error_response_type(self) -> str:
"""Return the response type name for error messages.
Examples: 'JSON response', 'response'
"""
if self._parser_name == "json":
return "JSON response"
elif self._parser_name == "xml":
return "response"
else:
raise ValueError(
f"Unknown parser_name: {self._parser_name}. Use 'json' or 'xml'."
)
Non-obvious 设计决策
- 代码对两种支持的解析器名采用显式枚举,而不是提供默认回退值;这使未识别配置不会被静默接受,而是立即经
ValueError暴露。 - 返回值不是机械地回显解析器名:
json被映射为较具体的"JSON response",而xml被映射为更短的"response";这是源码中明确写死的文案选择。 - 异常消息不仅说明只接受
'json'或'xml',还插入当前self._parser_name,便于定位实际收到的非法值。
上下游关系
- 调用方: 上层错误处理路径会调用本函数以取得错误消息中的响应类型标签
- 核心被调用: ValueError
- 配置/状态来源: self._parser_name
- 结果去向: 函数返回的字符串会被上层作为错误消息中的响应类型名称使用; 异常路径把包含未知解析器名的
ValueError交给调用方处理 - 同类 sibling: Terminus2._run_agent_loop:兄弟逻辑负责在解析错误时重建提示并记录轨迹;本函数只提供其中用到的类型标签,不处理提示拼装或状态记录。