llm_utils.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. # -*- coding: utf-8 -*-
  2. """LLM 输出解析辅助函数。"""
  3. import json
  4. import re
  5. from typing import Any, Dict, Optional
  6. _FENCED_JSON_RE = re.compile(r"```(?:json)?\s*([\s\S]*?)\s*```", re.IGNORECASE)
  7. # 回退正则:从类 JSON 结构中提取 "answer" 字段值,处理未转义控制字符等情况。
  8. _ANSWER_FIELD_RE = re.compile(
  9. r'"answer"\s*:\s*"((?:[^"\\]|\\.)*)"',
  10. re.DOTALL,
  11. )
  12. def extract_json_object(text: str) -> Dict[str, Any]:
  13. """从模型响应中提取 JSON 对象。"""
  14. if not text:
  15. return {}
  16. stripped = text.strip()
  17. fenced_match = _FENCED_JSON_RE.search(stripped)
  18. if fenced_match:
  19. stripped = fenced_match.group(1).strip()
  20. try:
  21. value = json.loads(stripped)
  22. return value if isinstance(value, dict) else {}
  23. except json.JSONDecodeError:
  24. pass
  25. start = stripped.find("{")
  26. end = stripped.rfind("}")
  27. if start >= 0 and end > start:
  28. fragment = stripped[start:end + 1]
  29. try:
  30. value = json.loads(fragment)
  31. return value if isinstance(value, dict) else {}
  32. except json.JSONDecodeError:
  33. # 重试时转义控制字符(模型常在字符串值中输出字面换行/制表符)
  34. repaired = _repair_control_chars(fragment)
  35. if repaired != fragment:
  36. try:
  37. value = json.loads(repaired)
  38. return value if isinstance(value, dict) else {}
  39. except json.JSONDecodeError:
  40. pass
  41. return {}
  42. def extract_answer_field(text: str) -> Optional[str]:
  43. """尽力从原始 LLM 响应中提取 "answer" 字段。
  44. 当 ``extract_json_object`` 解析失败时(如流式输出包含未转义控制字符),
  45. 作为回退方案使用。
  46. """
  47. if not text:
  48. return None
  49. match = _ANSWER_FIELD_RE.search(text)
  50. if not match:
  51. return None
  52. raw_value = match.group(1)
  53. # 解标准 JSON 转义序列
  54. try:
  55. return json.loads(f'"{raw_value}"')
  56. except json.JSONDecodeError:
  57. return raw_value
  58. def _repair_control_chars(s: str) -> str:
  59. """替换 JSON 字符串值中的字面控制字符。
  60. 模型有时会在字符串字面量中输出原始换行符/制表符,
  61. 导致 ``json.loads`` 报错。此函数将其替换为正确的转义序列,
  62. 同时保持周围 JSON 结构不变。
  63. """
  64. # 仅替换引号之间的控制字符。
  65. # 简单处理:将所有未转义的 \n/\r/\t 替换为转义版本,
  66. # 但跳过已转义的序列(前面有反斜杠的)。
  67. result = []
  68. i = 0
  69. in_string = False
  70. while i < len(s):
  71. c = s[i]
  72. if c == '"' and (i == 0 or s[i - 1] != "\\"):
  73. in_string = not in_string
  74. result.append(c)
  75. elif in_string and c == "\n":
  76. result.append("\\n")
  77. elif in_string and c == "\r":
  78. result.append("\\r")
  79. elif in_string and c == "\t":
  80. result.append("\\t")
  81. else:
  82. result.append(c)
  83. i += 1
  84. return "".join(result)
  85. def compact_json(value: Any) -> str:
  86. return json.dumps(value, ensure_ascii=False, indent=2)