在构建AI智能体的过程中,输出内容被截断是一个高频且隐蔽的故障。表面上看,Agent只是“话说到一半就停了”,但背后可能涉及上下文窗口超限、max_tokens参数设置过小、流式传输中断、工具调用结果过长等多种原因。如果不搞清楚截断的真实成因,盲目调大参数往往治标不治本。本文将从原理到实践,系统地梳理这个问题。

一、先搞清楚:内容为什么会被截断
很多开发者遇到截断问题的第一反应是“模型不行”,但实际上绝大多数截断都和模型的调用参数有关。要定位问题,需要先区分几种不同的截断场景。
第一种是最大输出Token限制。无论是OpenAI的GPT系列还是其他大模型,API中都存在max_tokens这类参数,它限制了单次响应的最大长度。当模型生成的文本达到这个上限时,响应会被硬性截断,通常表现为最后一句话戛然而止,甚至停在半个词语中间。finish_reason字段如果返回的是length而不是stop,就是这种截断的直接证据。
第二种是上下文窗口超限。模型的上下文窗口是输入加输出共享的总长度,如果Agent的历史对话、系统提示词、工具返回结果占用了大量Token,留给输出的空间就会被挤压。有些智能体框架在历史消息累积过长时,还会直接丢弃最早的消息或报错,导致模型“忘记”之前的任务要求,输出内容偏离预期。
第三种是流式传输中断。使用stream模式时,网络波动、代理超时、服务端断开都会导致流提前结束。这种截断的特点是客户端收到的内容突然停止,但没有任何错误提示,容易被误认为是Token限制问题。排查时可以先关闭流式模式,用非流式请求对比输出结果。
第四种是工具调用结果过长。Agent在执行工具(如网页抓取、数据库查询)后,把大段原始结果直接塞回上下文,既挤占了输出空间,也可能让模型在处理时草草收尾。
二、针对Token限制的核心解决方案
1. 检测截断并自动续写
最直接的思路是:先检测输出是否被截断,如果被截断,就把已有内容作为上下文发起续写请求,最后拼接结果。判断依据通常是finish_reason是否为length,或者输出长度是否恰好等于max_tokens。
import openai
def generate_with_continue(client, prompt, max_tokens=2000):
"""带自动续写的完整生成,防止输出被截断"""
full_text = ""
messages = [{"role": "user", "content": prompt}]
while True:
resp = client.chat.completions.create(
model="gpt-4o",
messages=messages,
max_tokens=max_tokens,
)
choice = resp.choices[0]
full_text += choice.message.content
# finish_reason 为 stop 表示模型自然结束
if choice.finish_reason == "stop":
break
# 被截断,把已生成内容拼进上下文请求续写
messages = messages + [
{"role": "assistant", "content": choice.message.content},
{"role": "user", "content": "请从刚才停止的地方继续,不要重复已输出的内容。"},
]
return full_text这个方案的要点在于续写提示词的写法。一定要明确告诉模型“从停止处继续,不要重复”,否则很多模型会重复输出最后一句话,拼接后出现内容重叠。更稳妥的做法是在拼接时对重叠部分做去重处理,例如检查新内容的前若干字符是否与旧内容的尾部重合。
2. 输出前先让模型规划结构
续写方案能救急,但更好的做法是让模型在生成长内容前先输出一份大纲,然后按章节逐段生成。这样每次调用的输出长度都可控,天然不会触发截断。这在多步骤Agent中尤其有效:把“写一份完整报告”拆解为“先列大纲,再逐章生成,最后汇总”。
def structured_generation(client, topic):
# 第一步:生成大纲
outline = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user",
"content": f"为主题'{topic}'生成报告大纲,只输出章节标题列表"}],
max_tokens=500,
).choices[0].message.content
# 第二步:逐章生成,每章输出长度可控
sections = outline.strip().split("\n")
report = []
for section in sections:
content = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "你是专业报告撰写者,针对指定章节深入展开"},
{"role": "user", "content": f"主题:{topic}\n章节:{section}\n请只写这一章的内容"},
],
max_tokens=1500,
).choices[0].message.content
report.append(f"## {section}\n{content}")
return "\n\n".join(report)这种结构化生成的额外好处是:每一章都是独立调用,任何一章出问题都可以单独重试,不用整篇重来。缺点是总耗时增加、上下文衔接不如一次性生成流畅,可以在最后追加一次“通读润色”调用来弥补。
3. 压缩上下文,给输出腾出空间
当问题是上下文窗口被挤占时,需要主动做上下文管理。常见手段包括:对历史对话做摘要压缩、限制工具返回结果的长度、把不常用的中间结果写到外部存储而不是保留在消息列表里。
def trim_messages(messages, max_history=10, max_tool_result=2000):
"""压缩消息历史,防止上下文超限"""
system_msgs = [m for m in messages if m["role"] == "system"]
history = [m for m in messages if m["role"] != "system"][-max_history:]
trimmed = []
for m in history:
# 工具结果过长时截断并附加提示
if m["role"] == "tool" and len(m["content"]) > max_tool_result:
m = {**m, "content": m["content"][:max_tool_result] + "\n...[结果已截断]"}
trimmed.append(m)
return system_msgs + trimmed如果使用了LangChain、LlamaIndex这类框架,可以直接启用自带的记忆管理组件,例如ConversationSummaryBufferMemory会在历史超过阈值时自动把旧消息总结成摘要,保留语义的同时大幅减少Token占用。
三、工程层面的加固措施
1. 流式传输的重连与校验
对于流式截断,建议在客户端实现finish_reason校验和超时重试。正常结束的流式响应,最后一个chunk的finish_reason应该是stop。如果流中断且没有收到带stop的chunk,就应该记录已收到的内容并发起续写。同时给HTTP请求设置合理的超时时间,避免代理层默认的60秒超时把长响应拦腰斩断。
2. 监控与告警
在生产环境中,强烈建议对所有调用记录finish_reason、prompt Token数和completion Token数。当length占比超过某个阈值(比如5%)时触发告警,这往往意味着max_tokens设置不合理或者任务复杂度上升了。这些数据也是后续优化提示词、拆分任务的依据。
3. 参数配置的最佳实践
几个值得坚持的配置习惯:max_tokens不要用模型的默认值,而是根据任务显式设置并留出余量;系统提示词中明确要求模型“输出前先估算篇幅,若内容过长则分点精炼”;对于JSON结构化输出,尽量要求紧凑格式并去掉冗余字段描述,JSON中的缩进和换行都会消耗大量Token。
四、方案选择的决策建议
不同场景下应该选择不同的组合策略。如果输出是自然语言长文(报告、文章),优先用结构化分章生成;如果是单次问答偶尔超长,自动续写检测就够了;如果Agent运行在多轮工具调用循环中,上下文压缩是必须项,否则跑不了几轮就会撞上窗口上限。
最后需要提醒一点:不要无脑把max_tokens调到模型允许的最大值。输出上限和上下文窗口是共享空间的,max_tokens越大,留给输入的空间越小,反而可能让历史消息被压缩得更厉害,引发“模型失忆”的新问题。合理的做法是根据实际任务的输出长度分布来设定,让截断只是偶发兜底情况,而不是常态。把检测、拆分、压缩、监控这几层措施组合起来,Agent输出截断的问题基本可以得到根治。