[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-51":3},{"id":4,"title":5,"title_en":6,"abstract":7,"abstract_en":8,"content":9,"content_en":10,"category":11,"banner_id":12,"banner_path":13,"tags":14,"is_recommend":16,"prev_article":17,"next_article":21,"created_at":25},51,"驯服大模型的输出：如何在 Go 中可靠地解析 LLM 返回的 JSON","Taming LLM Outputs: How to Reliably Parse JSON in Golang","在构建任何需要大模型做结构化输出的系统时，你迟早会遇到同一个问题：\n\n> **大模型不是数据库，它的","> - **Target Keywords:** Golang LLM JSON parsing, OpenAI JSON mode Go, json.Decoder vs json.Unmarshal, robust LLM integration Go, parse LLM response Golang.\n","在构建任何需要大模型做结构化输出的系统时，你迟早会遇到同一个问题：\n\n> **大模型不是数据库，它的输出是概率性的，而不是确定性的。**\n\n你在 Prompt 里写了\"请返回 JSON 格式\"，它有时候给你干净的 JSON，有时候给你 JSON 外面包一层 Markdown 代码块，有时候在 JSON 前面加一段解释文字，有时候在 JSON 后面补一句\"以上是我的回答\"，有时候干脆返回了格式完全错误的内容。\n\n如果你的代码对此没有任何防御，`json.Unmarshal` 就会直接报错，整个业务流程就断了。\n\n这篇文章系统性地梳理了在 Go 语言里可靠解析 LLM JSON 输出的完整方案，从最基础的防御手段到生产环境里的最终实践，逐层递进。\n\n---\n\n## 一、先搞清楚\"脏输出\"到底有多脏\n\n在开始讨论解决方案之前，我们先直视问题本身。LLM 的 JSON 输出究竟会脏到什么程度？\n\n### 常见的脏输出类型\n\n**1. Markdown 代码块包裹**\n\n这是最常见的情况，尤其是当你的 Prompt 里有代码示例时，模型会\"礼貌地\"把输出也包进代码块：\n\n```\n以下是您需要的 JSON：\n```json\n{\"action\": \"move\", \"target_x\": 10, \"target_y\": 5}\n```\n希望对您有帮助！\n```\n\n**2. 前缀废话**\n\n模型在 JSON 前面加了一段它认为\"有用\"的说明：\n\n```\n根据当前场景分析，我建议如下行动：\n{\"action\": \"talk\", \"speak\": \"今天天气不错\"}\n```\n\n**3. 后缀废话**\n\nJSON 之后还有内容：\n\n```\n{\"action\": \"stay\", \"target_x\": 0, \"target_y\": 0}\n\n注意：以上坐标为原地静止，如需移动请提供目标位置。\n```\n\n**4. 多个 JSON 对象**\n\n模型输出了多个 JSON 块，只有第一个是有效的：\n\n```\n初步决策：{\"action\": \"move\", \"target_x\": 3}\n备用方案：{\"action\": \"stay\", \"target_x\": 0}\n```\n\n**5. JSON 内部格式错误**\n\n最难处理的一类——结构本身有问题，比如多了逗号、字符串没闭合、或者字段名称写错了：\n\n```\n{\"action\": \"talk\", \"speak\": \"你好,}\n```\n\n了解了这些\"敌人\"的形态，才能有针对性地设计防御策略。\n\n---\n\n## 二、第一层防线：让模型不那么乱\n\n解析层面的防御固然必要，但如果能在源头减少脏输出的概率，问题就会简单很多。\n\n### 2.1 使用 JSON Mode（结构化输出强制模式）\n\n现代主流 LLM API 都提供了某种形式的\"强制 JSON 输出\"模式。在 OpenAI 兼容接口里（包括很多国内厂商），这叫做 `response_format`：\n\n```go\nreq := openai.ChatCompletionRequest{\n    Model: model,\n    ResponseFormat: &openai.ChatCompletionResponseFormat{\n        Type: openai.ChatCompletionResponseFormatTypeJSONObject,\n    },\n    Messages: []openai.ChatCompletionMessage{\n        {Role: openai.ChatMessageRoleUser, Content: prompt},\n    },\n}\n```\n\n启用 JSON Mode 之后，模型在大多数情况下会直接返回一个合法的 JSON 对象，不再附加额外的解释文字。\n\n**但这不是万能的。**\n\n首先，不是所有厂商都支持这个参数；其次，即使支持，也有模型会在 JSON 外面包一层 Markdown 代码块（这依然是合法的\"JSON 内容\"，只不过不是有效的 JSON 字符串）；第三，部分模型对这个参数的遵守程度因版本而异。\n\nJSON Mode 是重要的第一道防线，但绝不是最后一道。\n\n### 2.2 在 Prompt 里强化格式约束\n\n除了 API 参数，Prompt 的写法也很关键。以下几个技巧可以显著降低脏输出的概率：\n\n**明确说\"只返回 JSON，不要说其他的\"：**\n\n```\n请严格按照以下 JSON 格式返回你的决策，不要包含任何其他文字、解释或 Markdown 代码块：\n\n{\n  \"action\": \"move | stay | talk\",\n  \"speak\": \"如果是 talk，填写说话内容，否则留空字符串\",\n  \"target_x\": 目标X坐标（整数）,\n  \"target_y\": 目标Y坐标（整数）\n}\n```\n\n**给出一个具体的示例输出：**\n\n少样本学习（Few-shot）比纯粹的指令描述效果更好。在 Prompt 里附上一个或两个标准的示例 JSON，模型会更倾向于模仿格式：\n\n```\n示例输出（仅供格式参考，不要照抄内容）：\n{\"action\": \"move\", \"speak\": \"\", \"target_x\": 5, \"target_y\": 3}\n```\n\n**降低 Temperature（采样温度）：**\n\n`Temperature` 控制模型输出的随机性，数值越高输出越\"有创意\"，但同时格式遵从度越低。对于需要结构化输出的场景，把 `Temperature` 调低（0.1 ~ 0.3）会让模型更老实地遵守格式要求。\n\n---\n\n## 三、第二层防线：Go 代码里的解析防御\n\n即使做了第一层的防御，脏输出依然可能出现。这时候就需要在解析代码里下功夫了。\n\n### 3.1 最朴素的方案：直接 Unmarshal\n\n很多人第一反应是这样写：\n\n```go\nvar result MyStruct\nerr := json.Unmarshal([]byte(respText), &result)\nif err != nil {\n    return nil, err\n}\n```\n\n这段代码的问题是：任何一点脏内容（哪怕只是前面多了一个空格或一个字符），都会直接导致 Unmarshal 失败。脆弱性极高。\n\n### 3.2 进阶方案：找到第一个 `{`，用 Decoder 解析\n\n这是我们在生产环境里真正使用的方案，也是我认为最优雅的写法：\n\n```go\nfunc parseJSONResponse(respText string, target any) error {\n    \u002F\u002F 第一步：找到第一个 \"{\" 的位置\n    \u002F\u002F 这一步可以过滤掉 JSON 之前的所有前缀废话和 Markdown 标记\n    start := strings.Index(respText, \"{\")\n    if start == -1 {\n        return fmt.Errorf(\"LLM response contains no JSON object: %s\", respText)\n    }\n\n    \u002F\u002F 第二步：使用 json.Decoder 而非 json.Unmarshal\n    \u002F\u002F Decoder 天生只读取 Reader 中的第一个合法 JSON 对象\n    \u002F\u002F 遇到第一个完整的 JSON 对象后就停止，后续所有内容（废话、多余的 JSON）被自动忽略\n    decoder := json.NewDecoder(strings.NewReader(respText[start:]))\n    if err := decoder.Decode(target); err != nil {\n        return fmt.Errorf(\"JSON decode failed: %w, raw response: %s\", err, respText)\n    }\n\n    return nil\n}\n```\n\n**核心技巧一：`strings.Index(respText, \"{\")`**\n\n找到第一个 `{` 的位置，然后从这里开始解析。这一步能处理以下几类脏输出：\n- 前缀的说明文字\n- `````json` 之类的 Markdown 代码块标记\n- 各种开头的空白字符\n\n只需一行代码，过滤掉几乎所有的\"前缀污染\"。\n\n**核心技巧二：`json.Decoder` 替代 `json.Unmarshal`**\n\n这是整个方案里最关键的一个选择，值得多说几句。\n\n`json.Unmarshal` 要求整个输入是一个合法的 JSON 值。任何多余的内容都会报错。\n\n`json.Decoder` 则不同——它把输入当成一个流（Stream），从流的开头读取第一个合法的 JSON 值，读完之后立即停止，完全不在意后面还有什么。\n\n这意味着：\n- JSON 后面有废话？**没关系，Decoder 不读。**\n- JSON 后面还有另一个 JSON 对象？**没关系，Decoder 只取第一个。**\n- JSON 后面有 Markdown 的三个反引号？**没关系，Decoder 停在 JSON 结束的位置。**\n\n一句话总结：**`json.Decoder` 是宽容的流式解析器，`json.Unmarshal` 是严格的全量解析器**。对付 LLM 输出，前者是更合适的工具。\n\n---\n\n## 四、处理 JSON 数组的情况\n\n上面的方案针对的是 JSON 对象（`{...}`）。如果 LLM 返回的是 JSON 数组（`[...]`），只需要把查找的目标字符从 `{` 改成 `[`：\n\n```go\nfunc parseJSONArrayResponse(respText string, target any) error {\n    start := strings.Index(respText, \"[\")\n    if start == -1 {\n        return fmt.Errorf(\"LLM response contains no JSON array: %s\", respText)\n    }\n    decoder := json.NewDecoder(strings.NewReader(respText[start:]))\n    return decoder.Decode(target)\n}\n```\n\n如果不确定模型返回的是对象还是数组，可以同时找两个，取下标更小的那个作为起点：\n\n```go\nstartObj := strings.Index(respText, \"{\")\nstartArr := strings.Index(respText, \"[\")\n\nstart := -1\nif startObj != -1 && startArr != -1 {\n    if startObj \u003C startArr {\n        start = startObj\n    } else {\n        start = startArr\n    }\n} else if startObj != -1 {\n    start = startObj\n} else if startArr != -1 {\n    start = startArr\n}\n```\n\n---\n\n## 五、处理 Markdown 代码块的另一种思路\n\n有些场景下，模型一定会返回 Markdown 代码块（比如当你让它写代码的时候），此时\"找第一个 `{`\"的策略可能不够用。一个更明确的方案是先剥离 Markdown 代码块标记，再做 JSON 解析：\n\n```go\nfunc stripMarkdownCodeBlock(s string) string {\n    \u002F\u002F 去除 ```json 或 ``` 开头\n    s = strings.TrimSpace(s)\n    if strings.HasPrefix(s, \"```\") {\n        \u002F\u002F 找到第一行的结尾（即 ```json 这一行的末尾）\n        firstNewline := strings.Index(s, \"\\n\")\n        if firstNewline != -1 {\n            s = s[firstNewline+1:]\n        }\n    }\n    \u002F\u002F 去除末尾的 ```\n    if strings.HasSuffix(s, \"```\") {\n        s = s[:len(s)-3]\n    }\n    return strings.TrimSpace(s)\n}\n```\n\n这个函数和\"找第一个 `{`\"的策略可以叠加使用，形成双重过滤：\n\n```go\nclean := stripMarkdownCodeBlock(respText)\nerr := parseJSONResponse(clean, &result)\n```\n\n---\n\n## 六、当 JSON 本身格式有问题时\n\n以上方案都假设 JSON 的内容是合法的，只是外面有一些噪声。但如果 JSON 结构本身就破损了（缺括号、字符串未闭合等），就进入了最难的情况。\n\n这时候有几种思路：\n\n**方案 A：重试**\n\n最简单也是最实用的方案——遇到解析失败，直接重新调用 LLM。通常第二次或第三次模型会给出更规范的输出。\n\n```go\nvar result MyStruct\nvar lastErr error\nfor attempt := 0; attempt \u003C 3; attempt++ {\n    respText, err := callLLM(ctx, prompt)\n    if err != nil {\n        lastErr = err\n        continue\n    }\n    if err = parseJSONResponse(respText, &result); err == nil {\n        return &result, nil\n    }\n    lastErr = err\n    time.Sleep(time.Duration(1\u003C\u003Cattempt) * time.Second) \u002F\u002F 指数退避\n}\nreturn nil, fmt.Errorf(\"failed after 3 attempts: %w\", lastErr)\n```\n\n**方案 B：在 Prompt 里附上错误信息，要求模型修正**\n\n如果解析失败，把失败的原文和错误信息一起发给模型，让它自己修正：\n\n```\n你上一次的输出无法被解析为 JSON，原因是：[错误信息]\n\n你的原始输出是：\n[原文]\n\n请严格按照 JSON 格式重新输出，不要包含任何额外内容。\n```\n\n这个方案在某些情况下效果比盲重试更好，因为模型能\"看到\"自己的错误。\n\n**方案 C：使用第三方宽容解析库**\n\n对于一些结构性的小问题（比如末尾多了逗号），Go 生态里有一些宽容的 JSON 解析库可以处理，比如 `json5`。但引入额外依赖的代价和收益需要自己权衡。\n\n---\n\n## 七、生产实践中的完整流程\n\n综合以上所有技术，一个在生产环境中可用的 LLM JSON 解析流程大概是这样的：\n\n```\n调用 LLM API\n    ↓\n是否启用了 JSON Mode？（能用就用）\n    ↓\n去除 Markdown 代码块（如果需要）\n    ↓\n找到第一个 { 或 [\n    ↓\n用 json.Decoder 解析\n    ↓\n    ├── 成功 → 返回结果\n    └── 失败 → 带错误信息重试（最多 N 次）\n                ↓\n                继续失败 → 记录日志，返回错误，触发降级逻辑\n```\n\n**关于降级逻辑：** 如果所有重试都失败了，不要让整个程序崩溃。根据业务场景，可以返回一个合理的默认值（比如\"原地待机\"决策），同时记录详细的日志供后续排查。健壮性 > 完美性。\n\n---\n\n## 总结\n\n| 策略 | 解决的问题 | 成本 |\n|------|-----------|------|\n| JSON Mode (`response_format`) | 从源头减少脏输出 | 低（一个 API 参数） |\n| Prompt 格式约束 + 示例 | 进一步提高格式遵从度 | 低（Prompt 修改） |\n| `strings.Index` 找起点 | 过滤前缀噪声 | 极低（一行代码） |\n| `json.Decoder` 替代 `Unmarshal` | 忽略后缀噪声，只取第一个 JSON | 极低（三行代码） |\n| Markdown 代码块剥离 | 处理 ``` 包裹 | 低 |\n| 带错误信息的重试 | 处理偶发性格式错误 | 中（增加延迟和 API 成本） |\n\n没有哪一个方案是银弹。但\"**找第一个 `{` + 用 `json.Decoder` 解析**\"这个组合，用极低的代码复杂度处理了 90% 以上的脏输出场景，是我个人最推荐的基础防线。\n\n在这个基础上，加上 JSON Mode 参数和合理的重试机制，基本上就能应对生产环境里的各种情况。\n\n驯服 LLM 的输出，本质上是在和概率性系统打交道。保持足够的防御意识，同时不把代码写得过于复杂，才是最务实的工程态度。\n","# Taming LLM Outputs: How to Reliably Parse JSON in Golang\n\n> - **Target Keywords:** Golang LLM JSON parsing, OpenAI JSON mode Go, json.Decoder vs json.Unmarshal, robust LLM integration Go, parse LLM response Golang.\n> - **Meta Description:** Learn how to reliably parse JSON outputs from Large Language Models (LLMs) in Golang. Discover why `json.Decoder` beats `json.Unmarshal`, how to strip Markdown, and strategies for handling hallucinations and broken JSON structures.\n\nWhen building applications that require structured output from Large Language Models (LLMs), you will inevitably encounter a fundamental truth:\n\n> **LLMs are not databases; their output is probabilistic, not deterministic.**\n\nEven if your prompt explicitly states \"Please return JSON format,\" the model might give you clean JSON, or it might wrap it in a Markdown code block, prepend conversational filler like \"Here is the JSON you requested:\", append a polite \"Hope this helps!\", or just return completely malformed JSON.\n\nIf your code lacks defensive mechanisms and relies solely on `json.Unmarshal`, it will crash at the first sign of dirty output, breaking your entire business workflow.\n\nThis article systematically breaks down a complete, production-ready solution for reliably parsing LLM JSON outputs in Golang—starting from basic prompt engineering to resilient parsing techniques.\n\n---\n\n## 1. Understanding the Enemy: How \"Dirty\" Can LLM Outputs Get?\n\nBefore designing a defense, we must understand the types of hallucinations and formatting errors we are dealing with.\n\n### Common Types of Dirty JSON Outputs\n\n**1. Markdown Code Block Wrappers**\n\nThis is the most frequent issue, especially if your prompt includes code examples. The LLM will \"politely\" wrap its output:\n\n```text\nHere is the JSON you requested:\n```json\n{\"action\": \"move\", \"target_x\": 10, \"target_y\": 5}\n```\nLet me know if you need anything else!\n```\n\n**2. Conversational Prefixes**\n\nThe model adds introductory text before the JSON:\n\n```text\nBased on the current scenario, I recommend the following action:\n{\"action\": \"talk\", \"speak\": \"Nice weather today!\"}\n```\n\n**3. Conversational Suffixes**\n\nText appended after the JSON:\n\n```text\n{\"action\": \"stay\", \"target_x\": 0, \"target_y\": 0}\n\nNote: The coordinates indicate remaining stationary. Provide a target to move.\n```\n\n**4. Multiple JSON Objects**\n\nThe model outputs several JSON blocks, but only the first one is relevant:\n\n```text\nInitial thought: {\"action\": \"move\", \"target_x\": 3}\nAlternative plan: {\"action\": \"stay\", \"target_x\": 0}\n```\n\n**5. Malformed JSON Structure**\n\nThe hardest to handle: the structure itself is broken (e.g., trailing commas, unclosed quotes, or hallucinated property names).\n\n```json\n{\"action\": \"talk\", \"speak\": \"Hello there,}\n```\n\nKnowing these patterns allows us to build targeted parsing strategies.\n\n---\n\n## 2. The First Line of Defense: Constraining the Model\n\nWhile robust parsing is essential, reducing the probability of dirty outputs at the source makes everything easier.\n\n### 2.1 Enforce JSON Mode (Structured Outputs)\n\nModern LLM APIs often provide a \"JSON Mode.\" In OpenAI-compatible APIs (which many providers use), this is controlled via `response_format`:\n\n```go\nreq := openai.ChatCompletionRequest{\n    Model: model,\n    ResponseFormat: &openai.ChatCompletionResponseFormat{\n        Type: openai.ChatCompletionResponseFormatTypeJSONObject,\n    },\n    Messages: []openai.ChatCompletionMessage{\n        {Role: openai.ChatMessageRoleUser, Content: prompt},\n    },\n}\n```\n\nEnabling JSON Mode forces the model to return a valid JSON object in most cases, eliminating conversational filler.\n\n**However, this is not a silver bullet.**\nFirst, not all providers support it. Second, even when supported, some models might still wrap the output in Markdown backticks (which is technically valid \"JSON content\" for the model, but invalid for standard JSON parsers). Finally, adherence varies by model version. JSON Mode is a critical first step, but not the final safeguard.\n\n### 2.2 Prompt Engineering for Strict Constraints\n\nBeyond API parameters, your prompt design is crucial. Use these techniques to minimize hallucinations:\n\n**Explicit Instructions:**\n```text\nYou must output strictly in the following JSON format. Do not include any conversational text, explanations, or Markdown formatting.\n\n{\n  \"action\": \"move | stay | talk\",\n  \"speak\": \"Text to speak if action is 'talk', else empty string\",\n  \"target_x\": Target X coordinate (integer),\n  \"target_y\": Target Y coordinate (integer)\n}\n```\n\n**Few-Shot Prompting (Provide Examples):**\nProviding a standard example JSON heavily influences the model to mimic the exact structure.\n```text\nExample output (use for format reference only):\n{\"action\": \"move\", \"speak\": \"\", \"target_x\": 5, \"target_y\": 3}\n```\n\n**Lower the Temperature:**\n`Temperature` controls randomness. A high value makes the output \"creative\" but less compliant. For structural tasks, setting a low `Temperature` (e.g., 0.1 to 0.3) forces the model to strictly adhere to the requested format.\n\n---\n\n## 3. The Second Line of Defense: Resilient Parsing in Golang\n\nEven with constraints, dirty outputs can slip through. This is where your Go code must be resilient.\n\n### 3.1 The Naive Approach: Direct `Unmarshal`\n\nMany developers default to this:\n\n```go\nvar result MyStruct\nerr := json.Unmarshal([]byte(respText), &result)\nif err != nil {\n    return nil, err\n}\n```\n\nThe fatal flaw here is that `json.Unmarshal` expects the *entire* byte slice to be a single, valid JSON value. Even a single leading space or a conversational prefix will cause it to panic or return an error.\n\n### 3.2 The Pro Approach: Find `{` and Use `json.Decoder`\n\nThis is the battle-tested pattern we use in production. It is elegant and highly effective.\n\n```go\nfunc parseJSONResponse(respText string, target any) error {\n    \u002F\u002F Step 1: Locate the first \"{\"\n    \u002F\u002F This strips away all conversational prefixes and markdown formatting\n    start := strings.Index(respText, \"{\")\n    if start == -1 {\n        return fmt.Errorf(\"LLM response contains no JSON object: %s\", respText)\n    }\n\n    \u002F\u002F Step 2: Use json.Decoder instead of json.Unmarshal\n    \u002F\u002F Decoder treats the input as a stream and stops exactly after parsing the first valid JSON object.\n    \u002F\u002F Subsequent garbage (suffixes, extra JSON blocks) is simply ignored.\n    decoder := json.NewDecoder(strings.NewReader(respText[start:]))\n    if err := decoder.Decode(target); err != nil {\n        return fmt.Errorf(\"JSON decode failed: %w, raw response: %s\", err, respText)\n    }\n\n    return nil\n}\n```\n\n**Key Technique 1: `strings.Index(respText, \"{\")`**\nFinding the first `{` ignores conversational prefixes, Markdown tags like ````json`, and leading whitespace. With one line of code, you bypass 90% of prefix pollution.\n\n**Key Technique 2: `json.Decoder` vs `json.Unmarshal`**\nThis is the secret sauce. \n`json.Unmarshal` is a strict, full-payload parser.\n`json.Decoder` is a forgiving, streaming parser. It reads the input stream, extracts the *first* valid JSON object, and stops. \n- Conversational suffix? **Ignored.**\n- Multiple JSON objects? **Only the first is parsed.**\n- Trailing Markdown backticks? **Safely ignored.**\n\nIn short: Use `json.Decoder` for LLM outputs.\n\n---\n\n## 4. Handling JSON Arrays\n\nIf you expect the LLM to return an array (`[...]`), simply change the search target to `[`:\n\n```go\nfunc parseJSONArrayResponse(respText string, target any) error {\n    start := strings.Index(respText, \"[\")\n    if start == -1 {\n        return fmt.Errorf(\"LLM response contains no JSON array: %s\", respText)\n    }\n    decoder := json.NewDecoder(strings.NewReader(respText[start:]))\n    return decoder.Decode(target)\n}\n```\n\nIf the return type (Object vs. Array) is unpredictable, you can search for both and use the one that appears first.\n\n---\n\n## 5. Stripping Markdown Blocks (An Alternative Approach)\n\nFor models that stubbornly return Markdown code blocks, explicitly stripping them before parsing provides an extra layer of safety:\n\n```go\nfunc stripMarkdownCodeBlock(s string) string {\n    s = strings.TrimSpace(s)\n    if strings.HasPrefix(s, \"```\") {\n        \u002F\u002F Find the end of the first line (e.g., after ```json)\n        firstNewline := strings.Index(s, \"\\n\")\n        if firstNewline != -1 {\n            s = s[firstNewline+1:]\n        }\n    }\n    if strings.HasSuffix(s, \"```\") {\n        s = s[:len(s)-3]\n    }\n    return strings.TrimSpace(s)\n}\n```\n\nYou can chain this with the previous method for dual protection:\n```go\ncleanText := stripMarkdownCodeBlock(respText)\nerr := parseJSONResponse(cleanText, &result)\n```\n\n---\n\n## 6. What If the JSON is Structurally Broken?\n\nIf the JSON itself is malformed (missing brackets, unescaped quotes), the parser will fail. You have three main strategies:\n\n**Strategy A: The Retry Loop**\nThe most practical solution. If parsing fails, retry the LLM call. Models often correct themselves on the second try.\n\n```go\nvar result MyStruct\nvar lastErr error\nfor attempt := 0; attempt \u003C 3; attempt++ {\n    respText, err := callLLM(ctx, prompt)\n    if err != nil {\n        lastErr = err\n        continue\n    }\n    if err = parseJSONResponse(respText, &result); err == nil {\n        return &result, nil\n    }\n    lastErr = err\n    time.Sleep(time.Duration(1\u003C\u003Cattempt) * time.Second) \u002F\u002F Exponential backoff\n}\nreturn nil, fmt.Errorf(\"failed after 3 attempts: %w\", lastErr)\n```\n\n**Strategy B: Feedback Loop to the LLM**\nSend the error back to the model and ask it to fix it:\n```text\nYour previous output could not be parsed as JSON. Error: [Error Message]\nYour output was: [Original Output]\nPlease output strictly valid JSON without conversational text.\n```\n\n**Strategy C: Forgiving Parsers**\nYou can use third-party Go libraries like `json5` that tolerate trailing commas or unquoted keys. However, evaluate if the extra dependency is worth it for your use case.\n\n---\n\n## 7. The Production-Ready Workflow\n\nCombining these techniques, a robust production workflow looks like this:\n\n```\nCall LLM API \n    ↓\nIs JSON Mode enabled? (Yes, if supported)\n    ↓\nStrip Markdown wrappers (Optional)\n    ↓\nFind the first '{' or '['\n    ↓\nParse using json.Decoder\n    ↓\n    ├── Success → Return struct\n    └── Failure → Retry (with exponential backoff or LLM feedback)\n                ↓\n                Still Failing? → Log error, return default fallback state (Graceful Degradation)\n```\n\n**Crucial Note on Graceful Degradation:** If all retries fail, do not crash the application. Return a safe default value (e.g., an \"idle\" action for an agent) and log the raw output for debugging. Resilience > Perfection.\n\n---\n\n## Summary\n\n| Strategy | Problem Solved | Cost |\n| :--- | :--- | :--- |\n| **JSON Mode (`response_format`)** | Prevents hallucinations at the source | Low (API param) |\n| **Prompt Constraints & Few-Shot** | Increases structural compliance | Low (Prompt engineering) |\n| **`strings.Index` to find `{`** | Bypasses conversational prefixes | Very Low (1 line of code) |\n| **`json.Decoder` (vs `Unmarshal`)** | Ignores conversational suffixes & trailing garbage | Very Low (3 lines of code) |\n| **Markdown Stripping** | Handles stubborn markdown backticks | Low |\n| **Retry Loops** | Recovers from structurally broken JSON | Medium (Latency & API costs) |\n\nThere is no silver bullet when dealing with probabilistic systems like LLMs. However, the combination of **finding the first `{` and parsing with `json.Decoder`** resolves over 90% of parsing errors with minimal code complexity. \n\nTaming LLM outputs requires a defensive mindset. By combining strict prompt engineering with forgiving parsing logic, you can build Go applications that integrate LLMs reliably at scale.\n","AI",40,"https:\u002F\u002Fblog4-1316398321.cos.ap-nanjing.myqcloud.com\u002Fblog5\u002F20260703205119__山田.png",[15,11],"GO",false,{"id":18,"title":19,"title_en":20},50,"用 Go + Gemini Embedding + pgvector 给 AI Agent 造一个\"会遗忘的大脑","Building a Scalable AI Agent Memory System with Golang, Gemini Embeddings, and pgvector",{"id":22,"title":23,"title_en":24},52,"在 Go 里接入多个大模型厂商：一个兼容 OpenAI 协议的通用客户端设计","Building a Universal Multi-Provider LLM Client in Golang (OpenAI-Compatible)","2026-07-02T19:57:31.728233+08:00"]