Terminus 2 手册

错误分支

stage-4.6Error Branch2 个函数

开场解释

这个 Stage 在解决一个很具体的问题:LLM(大语言模型)这次回话没按约定格式返回,系统没法安全往下跑命令。它只处理“解析失败”这条分支,不处理别的异常。它的职责不是修命令,而是拦住错误输出、把错误原因喂回给 LLM、要求它重答一次。这样做的目的,是把一次“格式错了”的回复,变成下一轮可修复的输入,避免错误结果直接流入命令执行路径。

主流程

  1. 先判定:这是不是解析失败分支。

    进入条件很简单:feedback 里出现 "ERROR:"。这说明上一轮 LLM 的回复在结构上没过解析器,系统拿不到可靠的分析、计划或命令。 如果没有这个标记,就不走这里,而是继续正常的命令执行路径。

  2. 把这次失败,改写成下一轮的“纠错题”。

    这里会重建一个新的 prompt,核心意思是:

    • 你上一次的回复有解析错误
    • 错在哪,feedback 里已经写明
    • 请按指定的响应类型重新回答

    _get_error_response_type()(把“该用哪种回复格式”翻成给模型看的标签)就是为这个服务的。 这一步为什么重要?因为模型如果只听到“你错了”,往往会继续乱答;但如果明确告诉它“你应该产出哪一类结构化回复”,纠错成功率会高很多。

  3. 即使失败,也把这一步记进轨迹。

    系统会照样记一次 Step(一次运行轨迹里的步骤记录),并补上这一步的成本指标,比如 token cache 使用量和本步花费。 这里有个关键设计:记录的是 LLM 的原始文本,而不是解析后的结构化内容。原因很简单:这次本来就没解析成功,结构化结果不可信;但原始文本对追错和重试很有价值。以后回看轨迹时,你能看到模型当时到底说了什么,而不是只看到一句抽象的“解析失败”。

  4. 立刻中断本轮后续动作,回到外层循环。

    这条分支会直接 continue。意思是:

    • 不执行命令
    • 不做完成判定
    • 不走本轮剩下的控制逻辑

    系统带着刚刚重建的纠错 prompt,进入下一轮,让 LLM 重新答。 所以这个 Stage 本质上是一个短路保护:格式不对,就别碰终端(终端环境),先把回答修正。

状态流动

与前后 Stage 的衔接

上游交进来的,是一份待处理的模型回复及其解析反馈:如果前面已经发现 feedback 带有 "ERROR:",就说明“回复内容到了,但格式没法用”。本 Stage 产出的不是命令,而是一个新的纠错 prompt 和一条失败轨迹记录,然后直接回到外层循环,再向 LLM 发起下一轮;只有未命中这条错误分支时,系统才会继续走后面的命令执行路径。

函数细节2

Terminus2._run_agent_loopterminus_2.py:1378–1420 ↗

解析错误分支的轨迹记录段

stage 上下文: 本段是 _run_agent_loopstage-4.8 的错误分支:仅当局部变量 feedback 存在且包含字符串 "ERROR:" 时进入。它不尝试修复当前响应本身,而是把本轮失败响应记录进 trajectory,并把局部变量 prompt 改写为纠错提示,供后续循环代码继续使用。作为该 stage 的首个已翻译单元,它对应的是“解析失败时如何落盘与计量”的部分,而非正常解析/执行路径。

这段代码在干什么

feedback 指示解析错误时,这段代码生成一段新的纠错提示文本 prompt,文本中嵌入原始 feedbackself._get_error_response_type() 返回的响应类型标签。随后它基于 chat.total_*tokens_before_*cost_before 计算本步增量指标,并把原始 llm_response.content 连同一条只含该 promptObservation 追加到 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_namecontentreasoning_contentprompt_token_idscompletion_token_idslogprobs 以构造轨迹步骤; chat: ? — 局部输入;提供 total_cache_tokenstotal_costtotal_input_tokenstotal_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)+1timestamp=datetime.now(timezone.utc).isoformat()source="agent"model_name=llm_response.model_name or self._model_namemessage=llm_response.contentreasoning_content=llm_response.reasoning_contentobservation.results=[ObservationResult(content=prompt)]; 写入 Metrics(...),其中 prompt_tokens=chat.total_input_tokens - tokens_before_inputcompletion_tokens=chat.total_output_tokens - tokens_before_outputcached_tokens=cache_tokens_used if cache_tokens_used > 0 else Nonecost_usd=step_cost if step_cost > 0 else None,并携带 llm_response.prompt_token_idscompletion_token_idslogprobs; 执行 continue,跳过当前循环轮次本片段之后的代码

执行流

  1. 先用条件 feedback and "ERROR:" in feedback 检查是否进入解析错误分支;只有满足该条件才执行下面逻辑。
  2. 构造新的局部 prompt:固定前缀为“Previous response had parsing errors”,中间嵌入原始 feedback,结尾要求返回 self._get_error_response_type() 指定的正确响应类型。
  3. 从累计计数中减去本轮调用前基线,得到 cache_tokens_usedstep_cost 两个增量值,分别来自 chat.total_cache_tokens - tokens_before_cachechat.total_cost - cost_before
  4. llm_response.model_name 存在,则把它写入 self._last_response_model_name,使最近一次响应的模型名状态与本次原始响应保持一致。
  5. self._trajectory_steps 追加一条 Stepstep_id 取当前长度加一,timestampdatetime.now(timezone.utc).isoformat() 的 UTC ISO 字符串,source 固定为 "agent"model_namellm_response.model_name or self._model_namemessage 直接记录 llm_response.content,并附带 reasoning_content
  6. Stepobservation 只含一个 ObservationResult(content=prompt);其 metrics 记录输入/输出 token 增量、按正值条件保留的 cached_tokenscost_usd,以及 llm_response 自带的 token ids 与 logprobs
  7. 完成记录后立即执行 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_tokenscost_usd 在不为正时被写成 None 而非 0,对应代码中的 if ... > 0 else None;这是一种显式区分“本步没有正向增量/不值得记录”与“确实记录到零值”的表示策略。
  • 即便是解析错误,本段仍然追加完整 Step,并把纠错文本放入 observation.results;这使 trajectory 同时保存“模型给了什么”和“系统反馈了什么”,而不是仅保留内部错误状态。

上下游关系

  • 调用方: Terminus2._run_agent_loop(本 region 所属循环体); _run_agent_loop 内部在获得 feedbackllm_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 提前转入下一轮)
寄存器交互
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,异常消息包含未知的解析器名

执行流

  1. 先读取 self._parser_name,进入 if / elif / else 三分支选择。
  2. self._parser_name == "json" 时,直接返回字面量 "JSON response"
  3. self._parser_name == "xml" 时,直接返回字面量 "response"
  4. 其余取值统一走 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:兄弟逻辑负责在解析错误时重建提示并记录轨迹;本函数只提供其中用到的类型标签,不处理提示拼装或状态记录。