t-comm
Version:
专业、稳定、纯粹的工具库
163 lines (156 loc) • 6.15 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var _typeof = require('@babel/runtime/helpers/typeof');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var _typeof__default = /*#__PURE__*/_interopDefaultLegacy(_typeof);
/**
* 通用 LLM 输出 JSON 容错解析
*
* 用于解析大语言模型(AI Agent)输出的 JSON:通常 LLM 输出的内容有以下几种 "脏" 情况:
* 1. 直接就是合法 JSON
* 2. JSON 被自然语言包围("好的,下面是 JSON: { ... } 谢谢")
* 3. JSON 被 markdown 代码块包裹(```json { ... } ```)
* 4. 因 token 上限被截断(缺少闭合的 ] 和 })
*
* 本函数依次使用 5 层策略尽力解析:
* 策略 1: 直接 JSON.parse
* 策略 2: 提取第一个 `{` 到最后一个 `}` 之间的内容
* 策略 3: 贪婪匹配最外层 markdown 代码块
* 策略 4: 按括号平衡从第一个 `{` 开始匹配(应对尾部自然语言夹带)
* 策略 5: token 截断时尝试补齐闭合符号("、]、})
*
* @param content LLM 返回的原始字符串
* @param logTag 策略 5 触发时的日志前缀(默认 [AI]),便于上层区分调用方
*
* @example
* ```ts
* const result = parseLooseJson<{ name: string }>(aiResponseText);
* if (result) console.log(result.name);
* ```
*/
function parseLooseJson(content) {
var logTag = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '[AI]';
var trimmed = content.trim();
var tryParse = function tryParse(str) {
try {
var _result = JSON.parse(str);
if (_result && _typeof__default["default"](_result) === 'object') return _result;
} catch (/* ignore */_a) {/* ignore */}
return null;
};
// 策略 1: 直接解析
var result = tryParse(trimmed);
// 策略 2: 提取第一个 `{` 到最后一个 `}` 之间的内容
if (!result) {
var firstBrace = trimmed.indexOf('{');
var lastBrace = trimmed.lastIndexOf('}');
if (firstBrace !== -1 && lastBrace > firstBrace) {
result = tryParse(trimmed.slice(firstBrace, lastBrace + 1));
}
}
// 策略 3: 贪婪匹配最外层 markdown 代码块(从第一个 ``` 到最后一个 ```)
if (!result) {
var codeBlockMatch = trimmed.match(/```(?:json)?\s*([\s\S]*)```\s*$/);
if (codeBlockMatch) {
var inner = codeBlockMatch[1].trim();
result = tryParse(inner);
if (!result) {
var _firstBrace = inner.indexOf('{');
var _lastBrace = inner.lastIndexOf('}');
if (_firstBrace !== -1 && _lastBrace > _firstBrace) {
result = tryParse(inner.slice(_firstBrace, _lastBrace + 1));
}
}
}
}
// 策略 4: 按括号平衡从第一个 `{` 开始匹配
// 策略 2 是简单地取 "第一个{到最后一个}",可能因为尾部有自然语言而匹配到错误范围。
// 这里从第一个 `{` 开始,逐字符追踪括号层级和字符串上下文,找到顶层闭合位置。
if (!result) {
var startIdx = trimmed.indexOf('{');
if (startIdx !== -1) {
var depth = 0;
var inString = false;
var _escape = false;
var endIdx = -1;
for (var i = startIdx; i < trimmed.length; i += 1) {
var ch = trimmed[i];
if (_escape) {
_escape = false;
continue;
}
if (ch === '\\' && inString) {
_escape = true;
continue;
}
if (ch === '"') {
inString = !inString;
continue;
}
if (inString) continue;
if (ch === '{') depth += 1;else if (ch === '}') {
depth -= 1;
if (depth === 0) {
endIdx = i;
break;
}
}
}
if (endIdx > startIdx) {
result = tryParse(trimmed.slice(startIdx, endIdx + 1));
}
}
}
// 策略 5: JSON 被 token 截断时尝试修复
// AI 输出可能因为 token 限制在中途截断,导致 JSON 不完整(如缺少闭合的 "]}")。
// 尝试从第一个 `{` 取到字符串末尾,逐步补上缺失的闭合符号。
if (!result) {
var _startIdx = trimmed.indexOf('{');
if (_startIdx !== -1) {
var jsonCandidate = trimmed.slice(_startIdx);
// 移除尾部明显不属于 JSON 的内容(如 AI 在 JSON 后面追加的自然语言)
var lastJsonChar = Math.max(jsonCandidate.lastIndexOf('}'), jsonCandidate.lastIndexOf(']'), jsonCandidate.lastIndexOf('"'));
if (lastJsonChar > 0) {
jsonCandidate = jsonCandidate.slice(0, lastJsonChar + 1);
}
// 计算缺失的闭合符号
var depth2 = 0;
var arrDepth = 0;
var inStr = false;
var esc = false;
for (var _i = 0; _i < jsonCandidate.length; _i += 1) {
var _ch = jsonCandidate[_i];
if (esc) {
esc = false;
continue;
}
if (_ch === '\\' && inStr) {
esc = true;
continue;
}
if (_ch === '"') {
inStr = !inStr;
continue;
}
if (inStr) continue;
if (_ch === '{') depth2 += 1;else if (_ch === '}') depth2 -= 1;else if (_ch === '[') arrDepth += 1;else if (_ch === ']') arrDepth -= 1;
}
// 如果在字符串内,先关闭字符串
if (inStr) jsonCandidate += '"';
// 补上缺失的 ] 和 }
for (var a = 0; a < arrDepth; a += 1) jsonCandidate += ']';
for (var b = 0; b < depth2; b += 1) jsonCandidate += '}';
if (depth2 > 0 || arrDepth > 0 || inStr) {
var closeParts = [];
if (inStr) closeParts.push('1个"');
if (arrDepth > 0) closeParts.push("".concat(arrDepth, "\u4E2A]"));
if (depth2 > 0) closeParts.push("".concat(depth2, "\u4E2A}"));
// eslint-disable-next-line no-console
console.log("".concat(logTag, " parseLooseJson \u7B56\u75655: \u5C1D\u8BD5\u4FEE\u590D\u622A\u65AD\u7684 JSON (\u8865\u5145 ").concat(closeParts.join(''), ")"));
result = tryParse(jsonCandidate);
}
}
}
return result;
}
exports.parseLooseJson = parseLooseJson;