仅靠 Prompt 要求 JSON 格式不足以保证可靠解析,需在 Parser 端做确定性校验、带警告的 best-effort 解析,才能构建可信的 AI 数据管道。
大多数关于从 LLM 获取结构化数据的文章都止步于 prompt:让模型输出 JSON,也许再给它一个 schema,就完事了。这些是必要条件但并非充分条件——更难的问题出现在另一端,即那些必须信任返回结果的代码。我在为某 CRM(Anguardia)构建导入管道时遇到了这个问题,系统需要读取 AI 生成的目标企业调研报告,而最终这个解析器教给我的比 prompt 还多。
Prompt 要求的是一个固定的 markdown 结构——标题、表格、复选框任务:
<!-- anguardia-dossier v1 -->
# Dossier: <Company Name>
## Company
- Industry: <industry>
- Website: <url>
- Location: <city or region>
- Source: <cold | referral | inbound | research>
## People
| Name | Role | Email | Phone | LinkedIn |
|------|------|-------|-------|----------|
## Suggested tasks
- [ ] <task title> | due: <YYYY-MM-DD, optional>
## Suggested outreach
<the first message, under 150 words>
这只是容易的前 80%。任何有能力的模型都能可靠地遵循这样的结构。真正有趣的决策都存在于读取这些内容的解析器中。
/** Deterministic Dossier v1 parse. Always returns a dossier object + warnings. */
export function parseDossier(text: string): ParseResult {
const warnings: string[] = [];
const hasMarker = textContainsDossierMarker(content);
if (!hasMarker) {
warnings.push("Dossier marker not detected. Parsing best-effort.");
}
// ... parsing continues regardless
parseDossier 没有失败模式——它总是返回一个 dossier 对象和一个警告数组,即使输入看起来根本不像一个 dossier。模型的输出不是你能够控制的契约,所以把格式不正确的 dossier 当作错误处理,只会导致为"抱歉,请重试"构建第二套更差的 UI。尽最大努力的解析加上可见的警告能完成同样的工作,而不会走进死胡同。
if (!KNOWN_COMPANY_KEYS.has(key)) {
warnings.push(`Unknown company field ignored: ${bullet[1].trim()}`);
continue;
}
模型偶尔会添加一个没人要求的字段,或者拼错某个字段。默默地将它强行转换为最接近的已知字段,会导致企业的 Slack 账号被存成它的网站。解析器会丢弃任何无法识别的东西,并明确告知——这是一条用户能看到的警告,不是一个他们无法察觉的猜测。
对于格式错误但确实落入了正确字段的数据,同样适用这个逻辑:
if (DATE_RE.test(dueRaw)) {
dueDate = dueRaw;
} else {
warnings.push(`Malformed due date ignored: ${dueRaw}`);
}
一个不符合 YYYY-MM-DD 格式的截止日期不会被宽松解析——它会被丢弃,并附带一条警告。另一种方案(用一个模糊的日期解析器试图理解模型写了什么)会以没人注意到的方式失败,直到一个任务的截止日期悄无声息地变成了错误的日期。
Prompt 告诉模型不要猜测联系方式:
Never invent. Leave any field blank if you cannot verify it from a real source.
Do not guess emails, phone numbers, or names. Blank is always better than plausible.
这是必要的,但它只是一个请求,而非保证——没有任何东西能阻止模型忽略它。所以解析器独立地以同样的方式构建:空白的表格单元格保持为 null,而不是被强制转换成看起来像数据的那种空字符串。两层独立的逻辑共同同意"空白优于似是而非",这比单独任何一层都更有价值。
export const DOSSIER_MARKER_LEGACY = "<!-- founder-os-dossier v1 -->";
export const DOSSIER_MARKER = "<!-- anguardia-dossier v1 -->";
export const DOSSIER_MARKERS = [DOSSIER_MARKER, DOSSIER_MARKER_LEGACY] as const;
产品在格式发布后改了名字。与其迁移每个已经生成的 dossier,解析器只是永久地接受两个标记。第一行的一个 HTML 注释是一个廉价而持久的版本标签——比 schema 注册表更便宜,而且它能承受品牌重塑而无需任何人重新生成旧的研究报告。
如果你在解析任何 LLM 产出的内容并对其进行自动化操作:
这些都不专属于 CRM 或目标企业研究——它适用于任何模型输出成为其他东西要处理的记录的管道。Prompt 大部分时候能让输出大致正确。解析器才是让"大部分时候"可以安全自动化的东西。
如果你想看这个解析器对应的实际 prompt:免费的 Dossier v1 prompt——把它粘贴到 Claude、ChatGPT 或任何模型中,加上一个公司名称,无需注册。