【直播切片工作台】第30章:LLM 客户端与输出解析器

简介: 本章详解LLM客户端的三种调用形态(普通/结构化/思考模式)及容错输出解析器,重点实现`extractJSONArray`状态机扫描器,从噪声文本中精准提取JSON数组,保障模型输出不可信前提下的鲁棒解析。(239字)

第30章:LLM 客户端与输出解析器

本章把 internal/pkg/llm 的另一半讲完:Chat 的三种形态(普通/结构化/思考模式)、ChatOptions 的参数设计,以及 parse 系列容错解析器(ParseIndices、parse_clips、parse_ai_slice)——「模型输出不可信」这个前提如何转化为具体的解析防御工事。

流程

LLM 客户端(client.go,第 2 章已见构造)的调用面有四个方法,按「对输出确定性的要求」分层:

  • ChatCompletions:普通调用,返回完整响应体 RawMessage;
  • ChatStructured(服务接口层):temperature=0 + response_format=json_object + 关闭思考模式——用于 ASR 后处理这类需要严格 JSON 的场景(接口注释原文);
  • ChatThinking:显式 enable_thinking=true——AI 切片需要更深推理(DashScope/Qwen 的混合思考模式开关);
  • Chat:兜底普通对话。

单次请求的流程:校验 APIKey 与 messages 非空 → 组装 chatRequest(model/messages/temperature/response_format/enable_thinking 按需 omitempty)→ POST /chat/completions → 解析 chatResponse.Choices[0].Message.Content → 上游在 body 里嵌 error 结构时转错误(OpenAI 兼容协议的常见变体,Error *struct{Message,Type,Code})。

解析器(parse_indices.go 等)负责把 Content 文本变成 Go 结构:去 markdown 代码块包裹(json /JSON / ``)→ 尝试直接 Unmarshal → 失败则extractJSONArray` 从混杂文本中手工扫描出第一个平衡的 JSON 数组(处理字符串内引号、转义、嵌套深度的完整状态机)→ 再解析 → 全部失败才报错(错误信息带 256 字符截断的原文)。

实现

实现里最值钱的是解析状态机(extractJSONArray):LLM 经常无视「只输出 JSON」的要求,输出「好的,以下是结果:[2,5,9] 希望有帮助」——解析器必须能从垃圾话里捞出数组。扫描器维护三个状态:depth(括号深度)、inString(是否在字符串内)、escape(前一字符是否反斜杠);只有 depth 归零才是一个完整 JSON 值的边界。这个 80 行的手写扫描器比正则可靠得多(正则处理嵌套与转义是出了名的坑)。

其他细节:

  • 三种思考模式映射为三个方法:调用方声明意图(要严格还是要深思),客户端翻译为参数组合——思考开关是 DashScope 特有的顶层字段 enable_thinking,被封装在协议适配层内,换供应商时只改 client.go。
  • response_format json_object:OpenAI 协议的官方 JSON 模式,配合 temperature=0 把随机性压到最低;结构化场景(解析索引)用它,创意场景(写标题)不用。
  • 错误内嵌在响应体:部分兼容实现 200 状态也带 error 字段——客户端显式检查并转换,避免「成功状态里藏着失败」。
  • parse_clips.go / parse_ai_slice.go 与 ParseIndices 同构,分别解析 clips 数组与 AI 切片的复合结构(indices + title + description + topics),共享「剥代码块 → 直接解析 → 扫描兜底」三段式。
  • 每个解析器配全量表驱动测试:纯 JSON、markdown 包裹、前后噪声、截断、空串——LLM 输出的形态空间被测试用例固化。

📌 设计决策

  • 不用 Function Calling / Tool Use:JSON 模式 + 容错解析的组合对所有 OpenAI 兼容后端通用(DashScope、DeepSeek、本地 vLLM 都支持),Function Calling 的兼容面反而参差。
  • 解析容错做在客户端库而非调用方:三个业务(切片、后处理、chat)共用一套防御,调一次修三处。
  • 600 秒超时:思考模式 + 长上下文(16000 rune 窗口)的真实耗时在分钟级,超时过短会把合法推理掐死。

代码示例

容错解析的三段式(索引场景):

// internal/pkg/llm/parse_indices.go(节选)
// ParseIndices 从 LLM 文本中解析句段索引数组。
// 兼容:纯 JSON 数组、markdown 代码块包裹、前后夹杂说明文字。
func ParseIndices(content string) ([]int, error) {
   
    content = strings.TrimSpace(content)
    if content == "" {
   
        return nil, fmt.Errorf("LLM 返回内容为空")
    }
    // 去掉常见 markdown 代码块包裹。
    if strings.HasPrefix(content, "```") {
   
        content = strings.TrimPrefix(content, "```json")
        content = strings.TrimPrefix(content, "```JSON")
        content = strings.TrimPrefix(content, "```")
        if idx := strings.LastIndex(content, "```"); idx >= 0 {
   
            content = content[:idx]
        }
        content = strings.TrimSpace(content)
    }
    if indices, ok := tryUnmarshalIndices(content); ok {
   
        return indices, nil
    }
    // 兜底:截取首个 JSON 数组再解析。
    if extracted := extractJSONArray(content); extracted != "" && extracted != content {
   
        if indices, ok := tryUnmarshalIndices(extracted); ok {
   
            return indices, nil
        }
    }
    return nil, fmt.Errorf("无法解析 LLM 返回的索引 JSON: %s", truncate(content, 256))
}

状态机扫描器(从噪声文本中捞出完整数组):

// extractJSONArray 从文本中截取第一个完整 JSON 数组。
func extractJSONArray(s string) string {
   
    start := strings.Index(s, "[")
    if start < 0 {
   
        return ""
    }
    depth, inString, escape := 0, false, false
    for i := start; i < len(s); i++ {
   
        ch := s[i]
        if inString {
   
            if escape {
    escape = false; continue }
            if ch == '\\' {
    escape = true; continue }
            if ch == '"' {
    inString = false }
            continue
        }
        switch ch {
   
        case '"': inString = true
        case '[': depth++
        // ']' depth-- 归零即返回(见原文件)
        }
    }
    return ""
}

小结

  • 客户端三形态:结构化(json_object+关思考)、思考模式、普通对话——意图即方法。
  • 解析三段式:剥代码块 → 直接 Unmarshal → 状态机扫描兜底;错误带 256 字符现场。
  • 兼容性优先选 JSON 模式而非 Function Calling,覆盖所有 OpenAI 兼容后端。

思考题

  1. 若模型返回的 JSON 数组里有重复索引([2,2,5]),应该在解析器、业务层还是校验层去重?各自理由。
  2. enable_thinking 的输出(推理过程)会占 Content 吗?结构化模式关闭思考与输出纯净度的关系如何验证?

项目信息

  • GitHub仓库:Chyona/live-mixer
相关文章
|
9天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
3676 14
|
10天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
3977 44
|
10天前
|
iOS开发 Docker MacOS
【2026实测】Docker Desktop下载、安装、汉化、配置教程(附官网安装包)
Docker Desktop 是 Docker 官方推出的桌面容器管理工具,集成引擎、CLI、图形界面与 Kubernetes,支持 Win/macOS/Linux。2026 最新版 4.88.1 强化 ARM 原生运行、镜像加速及资源可视化,附中文汉化与国内镜像配置指南。
1834 6
|
5天前
|
云安全 人工智能 安全
|
5天前
|
存储 弹性计算 人工智能
2026 年阿里云双十一活动入口攻略,新客服务器优惠通道、三大会场入口梳理
综合来看,2026年阿里云双十一活动入口清晰: 阿里云轻量应用服务器、阿里云ECS云服务器、阿里云GPU云服务器、阿里云百炼大模型服务平台 活动会场、综合权益中心、99计划专场三大入口覆盖服务器、AI权益与续费特价;新用户通过控制台首页banner、权益中心、产品页三处路径进入专属专场,老用户通过控制台弹窗和费用中心权益中心进入老客专区,企业用户做企业实名后解锁专属满减与阶梯折扣。
657 0
|
4天前
|
Linux 开发工具 iOS开发
2026最新codex下载安装保姆级使用教程(下载+安装+登录+使用,含所有历史版本)
本文详解OpenAI Codex国内快速上手指南:提供Win/macOS/Linux全平台免安装包、npm/Homebrew等多方式安装、账号/API Key双登录、简体中文切换,覆盖下载、安装、汉化、使用及高频避坑(如微软商店失败、npm超时等),10分钟即可跑通。
848 1
|
11天前
|
安全 算法 定位技术
《高性价比人生指南》完整原版:普通人用最少的资源,过好这一生的底层逻辑~How To Live Better
📖《高性价比人生指南》是一份基于实证、拒绝焦虑的生存手册,倡导以“人生时薪”为视角,重新计算时间、金钱与精力的成本与收益。它不承诺逆袭,只教人算清账:省下的钱若耗掉时间便亏;买体验比买物品更值;睡眠是最高杠杆的健康投资;关系贵在情绪价值而非表面热络;学习重在改变行为而非自我安慰。核心理念:用低成本、高回报的微小行动,换取长期的选择权——不是成为谁,而是掌控自己人生的主动权。
2103 4
|
云安全 安全 网络安全
|
23天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
4132 56

热门文章

最新文章