让 LLM 写入 CRM 前先过结构与业务校验
文章建议将通话记录处理拆为结构化动作生成与写入前校验,明确负责人、截止日期、证据及信息缺失状态。保留原始转录、提示词版本、校验结果和拒绝原因,避免把模糊讨论自动转成业务承诺。
AI 深度解读
把销售通话交给 LLM,生成摘要并不难,难的是判断哪些内容可以真的写进 CRM。作者的结论是:模型只负责提出结构化动作,代码负责验收,业务规则决定能否执行。聊到「下个月再看看」,不能自动变成下月一号到期的任务。
这套思路值得做 Node.js 和 AI 应用的程序员参考:它把模型输出、业务判断和外部写入拆开,让错误有机会被拦住。不过,文章提供的是设计经验和 Python 示例,没有 Node.js 实现,也没有量化实验来证明收益。
摘要写得通顺,为什么还不能直接创建任务
作者面对的是 B2B SaaS 销售场景。团队需要的输出其实很少:下一步做什么、谁负责、确实提及时才填写的截止日期,以及通话中的依据。
困难在于,通话经常只有意向,没有承诺。客户说「我们应该下个月重新讨论安全问题」,并没有确认会议时间、负责人或具体任务。模型若主动补齐这些内容,摘要可能更完整,CRM 数据却已经失真。
摘要可以概括意思,业务动作必须有足够依据。
作者因此设置三道关卡:先确认返回值是合法 JSON,再检查字段和取值是否符合契约,最后由确定性规则决定是否允许自动写入。所谓确定性规则,就是相同输入按明确条件得出相同处理结果,不再让模型临场判断。
动作类型只有四种:create_task、update_opportunity、send_follow_up 和 no_action。其中 no_action 很关键,它让「没有足够信息采取行动」成为正常结果,而不是迫使模型每次交出一个任务。
下面的校验器解决的是第二道关卡:检查候选对象是否符合约定。
from typing import Any
ACTION_TYPES = {
"create_task",
"update_opportunity",
"send_follow_up",
"no_action",
}
REQUIRED_KEYS = {"action_type", "confidence", "evidence", "reason"}
def validate_action(candidate: Any) -> tuple[bool, str]:
if not isinstance(candidate, dict):
return False, "result is not an object"
if set(candidate) != REQUIRED_KEYS:
return False, "unexpected or missing fields"
if candidate["action_type"] not in ACTION_TYPES:
return False, "unsupported action type"
if not isinstance(candidate["confidence"], (int, float)):
return False, "confidence must be numeric"
if not 0 <= candidate["confidence"] <= 1:
return False, "confidence must be between 0 and 1"
if not isinstance(candidate["evidence"], list) or not candidate["evidence"]:
return False, "at least one evidence item is required"
if not all(isinstance(item, str) and item.strip() for item in candidate["evidence"]):
return False, "evidence items must be non-empty strings"
if not isinstance(candidate["reason"], str) or not candidate["reason"].strip():
return False, "reason is required"
return True, "ok"
要看清 set(candidate) != REQUIRED_KEYS:它同时拒绝缺字段和多字段;也要看清 evidence 的检查,它只保证存在非空字符串,没有核对引用是否真的出现在转录中。
契约可以用 JSON Schema 表达,也可以交给服务已有的校验库。关键边界是:模型返回的内容,在代码验收之前始终只是数据。
0.80 是自动处理门槛,不是正确率证明
原文的另一段示例通过 TextModel 协议隔离模型调用,只暴露 complete 操作。extract_action 发出窄指令,要求只返回 action_type、confidence、evidence、reason,禁止推断日期和负责人;随后用 json.loads 解析,再调用 validate_action。
通过结构检查后,还有一条规则:confidence < 0.80 就拒绝自动处理,返回的 Decision 用 accepted、payload、reason 记录结果。
这里的 0.80 不能理解为动作有 80% 的概率正确,更不能理解为客户有 80% 的概率购买。它只是应用选择的一条处理边界,需要拿人工标注的通话验证。
动作类型也应该影响审批策略。作者举例,某些团队可以自动创建任务,但即使置信分数够高,send_follow_up 仍然需要人工批准。示例代码没有实现这层区别。
作者还报告了一项经验:起初以为加长提示词能解决多数抽取错误,实际帮助小于预期;把 no_action 设为正式结果、拒绝没有支持摘录的输出,改善更明显。但原文没有给出样本量、错误率或对照数据,这只能作为经验判断。
证据摘录也有边界。客户说「如果预算批准,我们就安排演示」,其中确实出现了安排演示,但承诺带有条件。审核者仍需查看上下文,尤其是多人插话、条件句和转录缺失的情况。
真正要管住的是从模型返回到 CRM 写入的整条路径
作者建议的运行流程是:获取转录,移除 CRM 不需要的秘密信息,调用模型适配器,解析一次,校验,再保存不可变的处理记录。通过业务策略后,才让 worker 调用 CRM。
原始转录、模型响应、规范化动作、提示词版本和拒绝原因都应保留。它们回答的是不同问题:模型说了什么、程序接受了什么、最终为什么执行或拒绝。原始数据的保存与日志脱敏也要遵守访问规则。
重试同样需要分类。网络传输失败可以重试;已经成功返回、却不满足业务条件的结果,应该进入审核流程,不能靠反复调用赌一次通过。无法解析的条目要有死信去向,不能无限循环。
写入还必须具备幂等性,也就是同一次处理被重复执行时,不会反复创建任务。作者建议用通话标识、流水线版本和动作类型组成稳定键,让重试找到已有尝试。
批处理可以通过有界分组降低运行压力,但会改变销售看到结果的速度,以及失败条目的重放方式。原文提到 OpenAI Batch API 作为异步模式的例子,没有给出具体调用代码。校验、重放和 CRM 幂等仍由应用负责。
选择托管 API 还是自托管模型,也应放回这条路径里比较:前者涉及网络和数据治理约束,后者需要容量规划及模型运维。两者都无法修复含糊的动作契约。
评估时,作者建议从 100 条经过人工审核的样例起步,覆盖明确动作、明确无需动作、模糊承诺、多人对话、不同日期格式和缺失片段。这是起步建议,不是通用基准。
除了动作是否正确,还要分别看证据质量、弃权精确率、重复写入率、非法 JSON 比例和人工审核量。每次换提示词或模型,都跑同一份冻结样例,并重放历史转录,对比规范化动作,逐条检查发生变化的决策。
我的判断
我赞同把执行权留在应用代码里。相较于不断加长提示词,显式契约、拒绝路径和重放机制更容易检查,也更容易解释错误从哪里进入系统。结构化输出能力可以帮助约束格式,但业务授权仍需要单独判断。
不过,这份示例距离可用的 CRM 动作契约还有一段距离。作者强调负责人和截止日期,实际对象却没有对应字段,也没有任务内容、商机标识或动作专属参数。按当前结构,它更像动作分类结果,不能直接作为完整写入请求。
证据校验也只是起点。非空字符串既不能证明引用来自原文,也不能证明客户已经同意;模型自己填写的置信分数是否有效,文章同样没有验证。
幂等键则依赖业务前提。如果一通电话允许产生多个同类型动作,仅靠通话标识、流水线版本和动作类型,可能无法区分它们。原文没有说明这种情况如何处理。
因此,可以拿走它的边界设计,但不能据此认定 0.80 或这份 schema 已适合自己的团队。
能拿来做什么
- 在 Node.js 服务中,把模型适配器、动作校验和 CRM worker 分开实现;先补全各类动作必需的业务字段,再开放自动写入。
- 给审核界面同时展示拟执行动作、证据摘录和相关转录上下文,并把人工修正保存为后续评估样例。
- 从作者建议的 100 条样例起步,固定测试集;更换模型或提示词时,对比动作和拒绝结果,再检查审核量、延迟及 token 消耗。
- 为生成任务附上来源通话 ID 和处理版本,提供修改、取消入口,并保留关闭自动写入、继续只读抽取的开关。