输出长度限制
开场解释
这个环节是在解决一个很实际的问题:终端输出可能太长,长到把 LLM(大语言模型)的上下文挤爆,所以在 stage-4.9 Observe 里,必须先把要喂给模型的终端内容截短。它存在的目的不是“美化输出”,而是防止一次观察就吞掉太多 token(模型输入片段),让后面的推理还能继续。放在 Observe 这里也很自然:只有先看终端、整理观察材料时,系统才知道哪些文本真的要发给 LLM。
主流程
- 进入条件:Observe 要组装一份给 LLM 的观察材料
这一步会把终端里刚拿到的内容,整理成模型能读的上下文。问题是,终端输出有时会非常长,比如日志刷屏、错误堆栈很大、
cat了一个大文件。 如果原样塞进去,模型会被无关文本淹没。 - 先对终端输出做长度限制
这里会调用
_limit_output_length()(把终端文本压到上限内;超长就截断并加上[OUTPUT TRUNCATED]提示)。 它的职责很单纯:- 没超限:原样通过
- 超限:截掉尾部,并明确告诉模型“后面还有,但我没全给你”
这一步很重要,因为系统需要的不只是“少一点文本”,还要让模型知道这是被截断过的,避免它误以为终端本来就只输出了这些。
- 把“截短后的输出”放进 observation(观察文本)里
Observe Stage 真正负责的是“给 LLM 备料”。 所以长度限制不是终点,终点是生成一份可用的 observation:里面包含当前终端状态、命令结果、必要提示等,而终端输出部分已经被压到安全范围内。
- 然后交给后面的 LLM 推理
下一步消费的不是原始终端长文本,而是这份整理过、裁过长的 observation。 这样做的价值很直接:
- 控制上下文长度
- 保住重要信息的前半段
- 给模型一个明确的“这里被截断了”的信号
- 避免一次观察拖垮整轮决策
- 分支逻辑其实很简单,但意义很大
Observe 的主流程是“收集终端信息 → 整理 observation → 交给 LLM”。
_limit_output_length()只是其中一个小分支:- 输出短:直接用
- 输出长:截断后再用
但如果没有这个分支,Observe Stage 在日志很多、命令输出很大的场景下会变得不稳定,甚至让整轮 Agent 推理失真或超预算。
状态流动
- 写: 无 — 这一段不把结果落到 skeleton 明示的持久 register;它主要是在 Observe 过程中改写将要发给 LLM 的临时文本值
- 读: 无 — 输入里没有给出 skeleton 中可命名的 register;这里实际消费的是 Observe 阶段拿到的“终端输出”临时值
- 清: 无 — 这里只做裁剪,不负责清理状态
- 触发下游:
stage-4.9 Observe的后续 LLM 调用 — 当 observation 组装完成后,把其中已截短的终端内容继续交给模型
与前后 Stage 的衔接
上游先产出原始终端输出;到 stage-4.9 Observe 时,本环节把这段输出压到安全长度,再拼进 observation。下游消费的是这份“已截短、可进模型”的观察材料,而不是未经处理的终端原文。
Terminus2._limit_output_lengthterminus_2.py:530–566 ↗
按字节截断终端文本的辅助函数
stage 上下文: 该条目属于“Output Length Limiting”主题下的基础工具函数。就所给源码而言,它只负责把
output按max_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内,因为提示文本与两个换行符是额外加入的。
执行流
- 先执行
len(output.encode("utf-8")) <= max_bytes检查;若成立,立即返回原始output,不再做任何拆分或格式化。 - 若超限,则计算
portion_size = max_bytes // 2,把预算平均分给前后两侧;当max_bytes为奇数时,多出的 1 个字节不会分配给任一侧。 - 再次执行
output.encode("utf-8")得到output_bytes,后续所有截取均基于字节序列而非字符数。 - 从
output_bytes[:portion_size]解码得到first_portion,并从output_bytes[-portion_size:]解码得到last_portion;两次decode("utf-8", errors="ignore")都会忽略落在切片边界上的不完整多字节片段,因此得到的两段可能实际少于各自切片的字节数。 - 用
len(output_bytes) - len(first_portion.encode("utf-8")) - len(last_portion.encode("utf-8"))重新计算omitted_bytes,以反映真正未出现在返回字符串中的中部字节数。 - 返回格式化结果:
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_portion、last_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返回值返回给调用方