2266 字
约 7 分钟
6
大模型返回被截断怎么办?从分批处理到多轮续传

输入上下文很长,不代表模型可以一次输出很多;大批量结构化数据要优先分批,无法分批时再做续传和碎片恢复。

大模型返回被截断怎么办?从分批处理到多轮续传

做结构化数据提取时,很容易遇到一个问题:

输入明明没有超过上下文窗口
为什么返回的 JSON 还是只写了一半?

原因在于:

Context Window
≠
Max Output Tokens

上下文窗口决定模型一次能“看到多少”。

最大输出长度决定模型一次能“说多少”。

所以即使模型能够读取非常长的招聘文件,也可能输出到:

[
  {
    "company": "腾讯",
    "position": "Java开发"
  },
  {
    "company": "阿里",
    "position":

就直接结束。

这时不是 Structured Output 失效,而是 JSON 在生成过程中被截断了


1. 为什么 .entity() 救不了截断?

正常情况下:

List<Job> jobs = chatClient.prompt()
        .user(text)
        .call()
        .entity(new ParameterizedTypeReference<>() {});

流程是:

LLM
↓
完整 JSON
↓
Jackson
↓
List<Job>

但如果模型只返回:

[
  {...},
  {
    "company":

这已经不是合法 JSON。

因此:

Structured Output

只能解决:

模型应该按照什么结构返回。

解决不了:

模型有没有足够的输出空间把它全部返回。

这是两个完全不同的问题。


2. 最优方案:能拆就先拆输入

如果输入天然有边界,例如:

CSV
Excel
数据库记录
表格

最好的办法不是等模型被截断以后再抢救,而是在调用模型之前主动分批。

例如:

100 条数据
↓
每批 6 条
↓
Batch 1 → LLM
Batch 2 → LLM
Batch 3 → LLM
...
↓
合并结果

这样每次模型只需要返回少量 JSON:

小输入
↓
小输出
↓
完整 JSON
↓
直接反序列化

代码思路:

for (List<Row> batch : batches) {

    List<Job> result = chatClient.prompt()
            .user(buildPrompt(batch))
            .call()
            .entity(
                new ParameterizedTypeReference<>() {}
            );

    allJobs.addAll(result);
}

这其实就是经典的:

Chunking / Batch Processing

相比“先生成一个巨大 JSON,再想办法修”,这种方案通常更稳定。


3. 为什么分批是首选?

因为它同时解决几个问题。

假设一次让模型处理 100 条:

输出时间长
Token 多
容易超时
容易截断
JSON 越长越容易出错
失败后重试成本很高

拆成小批次以后:

6 条成功
→ 保存

下一批失败
→ 只重试这一批

因此生产环境里要形成一个习惯:

不要让一次 LLM 调用承担过大的工作单元。

这和传统后端批处理其实是一个思想。


4. 但不是所有数据都能拆

例如:

一篇很长的招聘公告
一个网页
一张图片
一段没有明显边界的自然语言

你不能随便从中间切一刀。

因为:

前半段:
“岗位要求如下……”

后半段:
“以上岗位工作地点均为武汉……”

强行拆分可能破坏语义。

这种场景就需要第二套方案:

多轮续传。


5. 多轮续传怎么做?

第一轮正常发送:

原始数据
+
结构化格式要求

如果结果明显没有结束,就继续问:

你之前返回的结果不完整,
继续返回剩余内容。

这时候需要:

ChatMemory

保存前几轮上下文。

整体:

原始数据
   ↓
LLM 第 1 轮
   ↓
部分结果
   ↓
ChatMemory
   ↓
“继续”
   ↓
LLM 第 2 轮
   ↓
剩余结果
   ↓
...

所以 ChatMemory 在这里不是为了普通聊天,而是:

让模型记得自己上一轮生成到了哪里。


6. 续传一定要设置退出条件

不能无限:

继续
继续
继续
继续

否则模型可能进入循环。

通常需要同时设置:

最大续传轮数
+
完整结果检测
+
重复生成检测

例如:

for (int i = 0; i < MAX_ROUNDS; i++) {

    String result = callModel();

    parse(result);

    if (isCompleted(result)) {
        break;
    }

    if (isRepeated(result)) {
        break;
    }
}

其中:

MAX_ROUNDS

就是最重要的安全阀。

所有自动续写、ReAct、Agent Loop 都必须设置最大迭代次数。


7. 最大难题其实是残缺 JSON

假设第一轮结束在:

{
  "company": "腾讯",
  "position": "Java开

第二轮可能继续:

发",
"salary": "25k"
}

这两段单独都不是合法 JSON。

所以一种比较实用的办法不是:

每一轮都强行解析整个 JSON 数组。

而是:

只提取当前已经完整的 JSON Object。

例如:

[
  {完整对象1},
  {完整对象2},
  {残缺对象3

先保存:

对象1
对象2

对象3留下,下一轮继续拼。


8. 括号计数提取完整对象

一个常见思路是遍历模型返回内容。

遇到:

{

层级:

+1

遇到:

}

层级:

-1

当层级重新回到 0:

说明一个 JSON Object 完整结束

例如:

{
  "company": "A",
  "job": "Java"
}

可以直接提取出来。

当然还必须处理:

字符串中的 { }
转义引号 \"
Markdown ```json

否则容易误判。

所以本质上是在做:

Output Fragment
↓
识别完整 Object
↓
立即解析保存
↓
残缺 Fragment
↓
等待下一轮

9. 不要追求“最后拼成一个巨大 JSON”

这一点其实很值得沉淀。

假设最终需要 100 条记录。

没必要:

5 轮返回
↓
拼成一个超级长 JSON
↓
最后一次性解析

更合理的是:

Round 1
→ 提取 20 条完整记录

Round 2
→ 再提取 18 条

Round 3
→ 再提取 21 条

直接不断:

itemList.add(...);

最终:

List<Job>

就是结果。

这比维护一个巨大字符串稳定得多。


10. AI 数据处理要接受 Partial Success

例如:

第一轮:成功 20 条
第二轮:成功 20 条
第三轮:成功 15 条
第四轮:模型超时

现在已经有:

55 条

一种错误处理方式是:

throw exception;

然后整个任务:

失败

前面 55 条全部丢弃。

很多 AI 数据采集场景更适合:

catch (Exception e) {
    break;
}

return itemList;

也就是:

Best Effort / Partial Success

能保留多少有效数据,就先保留多少。


11. 最终应该采用双策略

真正比较实用的设计不是只选一个方案,而是根据数据类型自动选择:

                 Input
                   ↓
            能不能安全拆分?
              /          \
            能             不能
            ↓               ↓
       Batch Processing   Multi-turn
            ↓            Continuation
       Structured Output      ↓
            ↓           Fragment Parser
            └───────┬─────────┘
                    ↓
                List<DTO>

例如:

CSV / Excel
→ 输入预拆分

网页 / 图片 / 长文本
→ 多轮续传

可以概括成一句:

能拆就拆,拆不了再续。


12. 和前面知识怎么串起来?

这个问题其实把之前很多 Spring AI 能力串起来了:

BeanOutputConverter
→ 告诉模型返回什么结构

ChatMemory
→ 让续传记住上一轮

Function Call
→ 必要时抓取网页数据

Tool / MCP
→ 获取外部内容

Fragment Parser
→ 恢复残缺 JSON

Batch Processing
→ 从源头控制输出大小

所以完整的数据采集链路可能是:

Input
 ↓
判断输入类型
 ↓
CSV / Excel ─────→ Batch
 ↓
Text / Web ──────→ Multi-turn
                       ↓
                 Function Call
                       ↓
                   Raw Data
                       ↓
                      LLM
                       ↓
               Structured Output
                       ↓
                Fragment Recovery
                       ↓
                  List<Record>

最后总结

这篇最值得沉淀的不是“怎么修 JSON”,而是 长输出治理思维

可以记住这一组原则:

  1. Context Window 和 Max Output Tokens 是两回事。
  2. 能预拆分的数据,优先 Batch,而不是生成超大 JSON。
  3. 无法拆分的数据,再使用 ChatMemory 做多轮续传。
  4. 每轮只提取完整 JSON 对象,不依赖一次得到完整数组。
  5. 续传必须有最大轮数和重复检测。
  6. AI 数据处理允许 Partial Success,保留已经成功的结果。

最值得记住的一句话:

处理 LLM 大批量输出时,不要赌模型“一次全吐完”;先把任务切小,实在切不了,再用续传和碎片恢复兜底。

这其实也是一个很通用的 AI 工程原则:

小任务
+
可恢复
+
可重试
+
部分成功

通常比一次巨大的 LLM 调用稳定得多。

大模型返回被截断怎么办?从分批处理到多轮续传
http://www.clxhxhhr.top/posts/666/
作者
clxstart
发布于
2026-09-17
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。