在前面的内容里,我们讨论了一些让大模型高质量输出内容的方法。其中让大模型输出 JSON 格式的数据,是一个非常有效且方便的方法。但是,当我们要进一步改善用户体验,希望通过流式传输减少等待时间时,就会发现 JSON 数据格式本身存在一个问题。对于从事前端行业的你来说,JSON 应该并不陌生,它是一种封闭的数据结构,通常以左花括号“{”开头,右花括号“}”结尾。封闭的数据结构,意味着一般情况下,前端对 JSON 的解析必须等待 JSON 数据全部传输完成,否则会因为 JSON 数据不完整而导致解析报错。这就导致一个问题,即使我们在前端用流式获取 JSON 数据,我们也得等待 JSON 完成后才能解析数据并更新 UI,这就让原本流式数据快速响应的特性失效了。那么有没有办法解决这个问题呢?JSON 的流式解析办法是有的。为了解决这个问题,有些人主张规范大模型的输出,比如采取 NDJSON(Newline-Delimited JSON)的方式,要求大模型输出的内容分为多行,每一行是一个独立的 JSON。但是这么做对大模型的输出进行了限制,不够灵活,而且很可能会影响大模型推理的准确性,有点得不偿失。另外一些人则使用 JSONStream 库,根据大模型输出的 JSON 配合 JSONStream 使用,这样能一定程度上解决问题,但是也不够通用,必须要事先针对大模型输出的特定结构进行处理,而且只能在 Server 端进行处理,没法直接在前端使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| @RestController @RequestMapping("/api/travel") public class TravelController {
private final ChatClient chatClient;
public TravelController(ChatClient chatClient) { this.chatClient = chatClient; }
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<Map<String, Object>> streamScenicSpots() { String prompt = """ 请依次介绍中国的5个著名景点。 每个景点单独输出一个JSON对象,不要合并。 格式如下: {"name": "景点名", "desc": "简介"} 每个景点之间用字符串 <END> 分隔。 """;
StringBuilder buffer = new StringBuilder();
return chatClient.prompt() .user(prompt) .stream() .flatMap(chunk -> { String text = chunk.getOutput().getContent(); buffer.append(text);
List<Map<String, Object>> ready = new ArrayList<>(); int idx; while ((idx = buffer.indexOf("<END>")) != -1) { String jsonChunk = buffer.substring(0, idx).trim(); buffer.delete(0, idx + "<END>".length()); try { Map<String, Object> json = new ObjectMapper().readValue(jsonChunk, Map.class); ready.add(json); } catch (Exception e) { } }
return Flux.fromIterable(ready); }); } }
|