Terminus 2 手册

输出长度限制

crosscut-X2Output Length Limiting1 个函数

开场解释

这个环节是在解决一个很实际的问题:终端输出可能太长,长到把 LLM(大语言模型)的上下文挤爆,所以在 stage-4.9 Observe 里,必须先把要喂给模型的终端内容截短。它存在的目的不是“美化输出”,而是防止一次观察就吞掉太多 token(模型输入片段),让后面的推理还能继续。放在 Observe 这里也很自然:只有先看终端、整理观察材料时,系统才知道哪些文本真的要发给 LLM。

主流程

  1. 进入条件:Observe 要组装一份给 LLM 的观察材料

    这一步会把终端里刚拿到的内容,整理成模型能读的上下文。问题是,终端输出有时会非常长,比如日志刷屏、错误堆栈很大、cat 了一个大文件。 如果原样塞进去,模型会被无关文本淹没。

  2. 先对终端输出做长度限制

    这里会调用 _limit_output_length()(把终端文本压到上限内;超长就截断并加上 [OUTPUT TRUNCATED] 提示)。 它的职责很单纯:

    • 没超限:原样通过
    • 超限:截掉尾部,并明确告诉模型“后面还有,但我没全给你”

    这一步很重要,因为系统需要的不只是“少一点文本”,还要让模型知道这是被截断过的,避免它误以为终端本来就只输出了这些。

  3. 把“截短后的输出”放进 observation(观察文本)里

    Observe Stage 真正负责的是“给 LLM 备料”。 所以长度限制不是终点,终点是生成一份可用的 observation:里面包含当前终端状态、命令结果、必要提示等,而终端输出部分已经被压到安全范围内。

  4. 然后交给后面的 LLM 推理

    下一步消费的不是原始终端长文本,而是这份整理过、裁过长的 observation。 这样做的价值很直接:

    • 控制上下文长度
    • 保住重要信息的前半段
    • 给模型一个明确的“这里被截断了”的信号
    • 避免一次观察拖垮整轮决策
  5. 分支逻辑其实很简单,但意义很大

    Observe 的主流程是“收集终端信息 → 整理 observation → 交给 LLM”。 _limit_output_length() 只是其中一个小分支:

    • 输出短:直接用
    • 输出长:截断后再用

    但如果没有这个分支,Observe Stage 在日志很多、命令输出很大的场景下会变得不稳定,甚至让整轮 Agent 推理失真或超预算。

状态流动

与前后 Stage 的衔接

上游先产出原始终端输出;到 stage-4.9 Observe 时,本环节把这段输出压到安全长度,再拼进 observation。下游消费的是这份“已截短、可进模型”的观察材料,而不是未经处理的终端原文。

函数细节1

Terminus2._limit_output_lengthterminus_2.py:530–566 ↗

按字节截断终端文本的辅助函数

stage 上下文: 该条目属于“Output Length Limiting”主题下的基础工具函数。就所给源码而言,它只负责把 outputmax_bytes 预算裁剪为“首段 + 中部提示 + 尾段”的字符串,不读取也不写入任何实例状态。当前提供的材料未展示其具体调用点,因此此处不扩展系统级触发条件。

这段代码在干什么

函数接收 output: str 与字节上限 max_bytes;若 output.encode("utf-8") 的长度未超限,则直接返回原字符串。若超限,则保留按字节切出的前半段与后半段,在两者之间插入一段带文字说明的标记,并以字面量换行符连接,返回形如 first_portion + "\n[... output limited to {max_bytes} bytes; {omitted_bytes} interior bytes omitted ...]\n" + last_portion 的新字符串。函数本身无副作用,也不触碰任何 self._* 状态。

接口 · 参数 / IO

(self, output: str, max_bytes: int = 10000) -> str

  • 参数: output: str — 待检查并可能截断的文本输入; max_bytes: int — 用于决定是否截断、以及前后各保留多少字节的预算上限
  • 返回: 若 output 的 UTF-8 编码长度 <= max_bytes,返回原始 output;否则返回由首段、字面量换行、[... output limited to {max_bytes} bytes; {omitted_bytes} interior bytes omitted ...] 提示、再次换行、尾段拼接而成的新字符串。该返回值的总长度并不被严格限制在 max_bytes 内,因为提示文本与两个换行符是额外加入的。

执行流

  1. 先执行 len(output.encode("utf-8")) <= max_bytes 检查;若成立,立即返回原始 output,不再做任何拆分或格式化。
  2. 若超限,则计算 portion_size = max_bytes // 2,把预算平均分给前后两侧;当 max_bytes 为奇数时,多出的 1 个字节不会分配给任一侧。
  3. 再次执行 output.encode("utf-8") 得到 output_bytes,后续所有截取均基于字节序列而非字符数。
  4. output_bytes[:portion_size] 解码得到 first_portion,并从 output_bytes[-portion_size:] 解码得到 last_portion;两次 decode("utf-8", errors="ignore") 都会忽略落在切片边界上的不完整多字节片段,因此得到的两段可能实际少于各自切片的字节数。
  5. len(output_bytes) - len(first_portion.encode("utf-8")) - len(last_portion.encode("utf-8")) 重新计算 omitted_bytes,以反映真正未出现在返回字符串中的中部字节数。
  6. 返回格式化结果:f"{first_portion}\n[... output limited to {max_bytes} bytes; {omitted_bytes} interior bytes omitted ...]\n{last_portion}"。其中两侧保留内容之间固定插入两个字面量换行符和一段说明文字。

源码

    def _limit_output_length(self, output: str, max_bytes: int = 10000) -> str:
        """
        Limit output to specified byte length, keeping first and last portions.

        Args:
            output: The terminal output to potentially truncate
            max_bytes: Maximum allowed bytes (default 10000)

        Returns:
            str: Original output if under limit, or truncated with middle omitted
        """
        if len(output.encode("utf-8")) <= max_bytes:
            return output

        # Calculate portions (half each for first and last)
        portion_size = max_bytes // 2

        # Convert to bytes for accurate splitting
        output_bytes = output.encode("utf-8")

        # Get first portion
        first_portion = output_bytes[:portion_size].decode("utf-8", errors="ignore")

        # Get last portion
        last_portion = output_bytes[-portion_size:].decode("utf-8", errors="ignore")

        # Calculate omitted bytes
        omitted_bytes = (
            len(output_bytes)
            - len(first_portion.encode("utf-8"))
            - len(last_portion.encode("utf-8"))
        )

        return (
            f"{first_portion}\n[... output limited to {max_bytes} bytes; "
            f"{omitted_bytes} interior bytes omitted ...]\n{last_portion}"
        )

Non-obvious 设计决策

  • 长度判定与切分都以 UTF-8 字节数而非 Python 字符数为准,体现的是“按编码后体积限额”而不是“按字符数限额”的策略;这直接对应 encode("utf-8") 的使用。
  • 边界解码采用 errors="ignore",容忍字节切片把一个多字节字符切断;代价是 first_portionlast_portion 可能都比各自的 portion_size 更短,因此代码没有直接假设两侧各保留了精确的一半字节,而是重新 encode 后计算 omitted_bytes
  • 函数在超限路径里先为早退检查执行一次 output.encode("utf-8"),随后又再次编码为 output_bytes 供切片使用;这是实现上的直接写法,避免复用前一次结果的额外分支组织,但代价是重复编码一次。
  • 返回值并非“硬性不超过 max_bytes”;max_bytes 只约束前后保留切片的预算,插入的提示文本与两侧换行是额外附加的。因此该函数追求的是保留头尾上下文并显式标记删减,而不是严格的最终字符串总字节上界。

上下游关系

  • 调用方: 未在所给源码片段中展示
  • 核心被调用: str.encode("utf-8"); bytes.decode("utf-8", errors="ignore"); len(...); f-string 字符串格式化
  • 配置/状态来源: 参数 output; 参数 max_bytes; 函数内部常量字符串 [... output limited to {max_bytes} bytes; {omitted_bytes} interior bytes omitted ...]; UTF-8 编码规则与 errors="ignore" 解码策略
  • 结果去向: 直接作为该函数的 str 返回值返回给调用方