news 2026/9/19 20:24:58

用LLM从沟通记录中结构化提取客户信息并写入CRM的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用LLM从沟通记录中结构化提取客户信息并写入CRM的工程实践

我接这个需求的时候,客户那边给到的诉求其实很简单:市场部和销售部每天产生大量的企微聊天记录、邮件往来和通话转写文本,以前全靠业务助理一条条看完,再手工把客户信息、商机进展、下一步跟进时间录进CRM。一个月下来,光录单就占掉一个助理大半的工作量,而且录错、漏录是常态。

所以目标就变成了——用LLM对非结构化沟通记录做结构化提取,再批量写入CRM。听起来很直接,真正开工才发现这里的坑比预想的多得多:提示词怎么约束、返回的JSON不稳定怎么办、批量写入怎么保证不重复、Token成本会不会失控。这篇文章把我从设计到落地全过程的思路、踩坑和最终方案完整复现一遍,给正在做类似事情的人一个可参考的工程路径。

1. 为什么规则引擎做不了这件事:从一次失败的抽取说起

1.1 沟通记录的多样性远比想象中复杂

先看一条典型的沟通记录,这种文本在真实业务里每天都在产生:

“李总说下周三下午来公司看演示,预算大概三十万左右,主要看我们的私有化部署能力,还说如果合适会带技术负责人一起来。对了王哥说他已经在OA上提交了合作意向,但具体金额还没定,让我周三前给个初步方案。”

如果让你手动抽取字段,你会得到:客户联系人“李总”、时间“下周三下午”、金额“三十万左右”、意向产品“私有化部署”、关键动作“带技术负责人来看演示”、辅助联系人“王哥”、任务“周三前给初步方案”。

但让规则引擎来做这件事,麻烦就来了:

  • “下周三”是一个相对时间,需要基于消息当天日期换算,正则只能匹配“周几”,换算逻辑要额外写。
  • “三十万左右”不是精确值,带模糊修饰词,直接抽出来是“三十万左右”,入库前要清洗。
  • “主要看我们的私有化部署能力”和“客户预算”是什么关系?规则无法理解这层语义。
  • 还有隐含信息——“如果合适会带技术负责人一起来”,这说明跟进阶段可能在“方案演示”和“技术评审”之间,这种阶段判断规则引擎基本做不了。

类似文本再叠加上不同销售的表达习惯、不同地域的口语化措辞、键盘输入导致的错别字,规则引擎的匹配字典会无限膨胀。

1.2 规则引擎的维护成本曲线

在决定上LLM之前,我们其实先用正则表达式做过一轮抽取原型。第一批20条规则处理100条测试样本,准确率能到80%出头。我当时觉得还行,毕竟写的都是核心字段,漏的也都是边角信息。

问题出在第二个团队接入之后。每个团队带来的沟通记录风格差异很大:有的销售习惯用短句分条发消息,有的习惯大段文字,还有人喜欢在中间插语音转写片段。规则从20条涨到80条,准确率反而跌到70%以下。原因很典型——规则冲突和误匹配:

  • 一条匹配客户电话的正则,在某个文本里误伤了一个QQ号,导致客户手机号被清洗成错误值。
  • 新增的金额规则和原有的日期规则在“预算30万,下周三定”这种句子里相互干扰,抽取结果前后矛盾。

维护这套正则的时间每周都要投入三四个小时,而且每次调整都存在“修了A场景踩了B场景”的风险。这个成本曲线是不可持续的。所以我做了一个ROI对比:继续维护规则引擎 vs 引入LLM抽取。对比下来LLM在一次性接入成本上更高,但长期维护成本和跨团队复制成本都低一个量级。

1.3 转向LLM后的能力边界判断

这里必须强调一个工程上的清醒认识——LLM不是用来解决所有问题的。我的方案里LLM只承担“从自由文本中抽取信息并映射为标准化中间字段”这一件事,其他工作全部交给代码:

  • 收到文本先做清洗(去签名、去转发链、URL归一化)。
  • LLM输出的原始结果先做JSON Schema校验,不合格就触发修复链路。
  • 字段里的枚举值(客户来源、产品线)由代码再做一轮映射,不依赖LLM的记忆。
  • CRM写入做幂等和去重,和LLM完全无关。

为什么这样切?因为LLM擅长的是“把语义理解转化为结构化表达”,但它的输出天然带有概率性,不擅长做精确匹配和状态判断。把所有确定性逻辑从LLM中剥离出去,系统的可观测性和稳定性才会真正可控。

2. 整体链路设计:从碎片文本到CRM字段的全管道

2.1 输入侧的清洗与上下文组装

原始沟通记录不能直接扔给LLM。我踩过的第一个教训就是:喂进去什么,很大程度决定了输出的质量。原始文本里有大量与业务无关的内容,典型的有:

  • 邮件签名档、公司免责声明。
  • 转发链里的“-----原始邮件-----”内容和历史上下文。
  • 企微表情代码、URL、无关的自动通知。

这些噪音不仅浪费Token,还会干扰抽取准确率。所以输入侧我加了一个清洗层,规则不复杂,但收益很明显:

  1. 去掉回复引用链中超过两层的部分,只保留最近一层的上下文。
  2. 用正则剔除URL、邮箱签名、固定免责声明文本。
  3. Emoji统一替换为对应语义标签(比如🕐替换为[时间标记]),避免特殊字符打乱切分。
  4. 超过1400字的文本先做摘要压缩,压缩时要求保留“客户、金额、时间、联系人、异议、承诺”等关键信息。

清洗之后的文本干净很多,后面的提示词和输出稳定性都有了基础。

2.2 关键设计决策:一次一记录,而不是批量抽取

这是整个方案里我认为最核心的决策——每条沟通记录单独调用一次LLM,而不是把10条记录打包成一个数组让LLM一次性输出

我知道批量调用的单次成本低、吞吐量高,看起来是更优解,但在工程实践里有三个绕不开的问题:

第一是错误隔离。10条记录拼进一个Prompt,只要其中一条触发幻觉,输出的整个JSON数组可能全部解析失败,或者某条数据污染了相邻记录。单条调用时,失败只会影响当前这一条,重试成本和控制逻辑都简单。

第二是上下文干扰。连续多条不同客户的记录放在一起,LLM在抽取时容易出现字段串位:把A客户的时间安到B客户头上。尤其当记录里出现“王哥”“李总”这种相似称呼时,串位概率会明显上升。

第三是稳定性。我在测试阶段做过对比,单条记录抽取时JSON合法率在98%以上,批量拼接输出数组时合法率掉到90%左右。批量时一旦中间某条文本触发了模型的不稳定行为,整个返回都作废。对于生产系统来说,5%的解析失败率意味着每天要多出大量重试任务,省下的Token又被重试吃回去了。

2.3 输出侧的目标CRM字段映射策略

LLM直接输出CRM字段是不可取的。各家CRM的字段体系高度定制化,而且经常出现“一个字段在系统里拆成两个”“两个不同来源的值要合并到一个字段”这类情况。所以我在LLM和CRM之间加了一个标准中间层

LLM的输出统一走一套中间Schema,字段全部用英文命名,值只能是字符串、数字或null。然后映射层负责:

  • 枚举翻译:LLM输出“私有化部署”,映射为产品线ID=3。
  • 日期解析:LLM输出“下周三下午”,由代码结合消息发送日期计算成标准ISO时间。
  • 负责人归属:根据消息发送人、客户群归属计算CRM中的负责人ID。

这样设计的最大好处是,哪天换了CRM系统,只需要重写映射层,LLM侧的提示词和Schema完全不用动。

3. 提示词与JSON Schema约束:让模型稳定输出结构化数据的核心

3.1 提示词结构设计:角色、任务、格式、示例缺一不可

提示词是我调试时间最长的地方。一个稳定可用的结构化抽取提示词,我的最终结构长这样:

你是客户信息抽取助手。我会给你一段真实的客户沟通记录,请提取其中与客户跟进相关的字段。 要求: 1. 只输出JSON,不输出任何解释性文字。 2. 如果某个字段在原文中没有对应信息,输出null,禁止猜测。 3. 保持字段值使用原文中的原文表述,不要改写。 4. 金额字段只输出数字和单位,不要添加判断性语言。 请严格按照以下JSON Schema输出: {...schema内容...} 以下是几个抽取示例: {...few-shot示例...} 待抽取的沟通记录: {清洗后的原始文本}

几个细节值得展开说:

  • 第2条“禁止猜测”非常关键。LLM有一种倾向,就是字段缺失时会根据常识补一个“合理值”,这在抽取场景里是灾难。这条指令配合Schema里的null约束,能把幻觉率压到很低。
  • 第3条“保持原文表述”,是为了避免模型把“三十万左右”改写成“300000”,因为改写过程可能引入计算错误。金额换算统一放到映射层做。
  • 强调“只输出JSON”,是为了减少后续解析时的文本清理工作量。

3.2 JSON Schema校验与Temperature参数的关系

Schema不只是给模型看的,更是给程序看的。我在提示词里内嵌了完整的JSON Schema定义,同时在后端用校验器对模型输出做二次校验。这里有个很容易忽略的点:校验器必须做字段级类型检查,而不仅是检查“这段文本能不能被解析成JSON”

举个例子,模型输出了{"budget": "三十万左右"},JSON解析是成功的,Schema里如果定义budget为string类型,校验也通过了。但映射层如果想计算总金额,这个字符串就完全没法参与计算。所以中间Schema的设计里,budget我用的是number类型加percent提示,如果模型输出非数字,校验直接失败进入修复链路。

Temperature参数也值得说说。很多人知道抽取场景要把Temperature调低,但不知道为什么。简单解释一下:LLM生成时通过Softmax把token得分转换为概率分布,Temperature就是用来缩放这个分布的。Temperature越高,分布越平坦,低概率token被采样到的可能性越大,输出就越“发散”。在结构化抽取里,我们需要的是高概率的确定性输出,所以Temperature设在0到0.2之间最合适。设成0时输出基本是贪心解码,稳定但可能略显生硬;0.2左右会保留一点点多样性,更适合有少量口语化变体的字段。

3.3 few-shot示例的选样原则

Few-shot示例不是越多越好。我最初放了6个完整示例,覆盖各种场景,结果Token消耗大且边际收益几乎为零。后来压到3个示例,准确率反而没下降,这说明了选样比数量重要。

我的选样原则是让示例之间有区分度

  • 示例一:字段齐全的标准记录,让模型看到完整输出的样子。
  • 示例二:一半字段缺失,输出里对应字段为null,教模型“宁可空着也不要编”。
  • 示例三:包含模糊表达和相对时间(如“这两天”、“下周左右”),教模型保留原文表述而不自行推断。

少放重复度高的完整模板,因为模型看多了同质化示例并不会提升理解能力,只会增加Token成本。

4. JSON解析容错:LLM返回不合法JSON时怎么办

4.1 常见的非法JSON类型与根因

不管提示词怎么约束,LLM偶尔还是会返回不合法JSON。生产环境跑了一个月之后,我汇总了非法返回的主要类型:

非法类型出现频率典型示例
输出被Markdown围栏包裹json {…},前面还带了一个“结果如下:”
字段值包含未转义引号"remark": "他说"好的"然后挂了"
布尔值大小写混用"has_decision": True(Python风格)
输出在结尾被截断JSON最后一个括号缺失
前后混入解释性文本JSON前后夹杂“我已抽取完毕”等说明

根因上,Markdown围栏和解释文本大多是模型“过拟合”了训练时的格式习惯,尤其当训练语料里大量出现“以下是结果”这类过渡句时。未转义引号多出现在原文包含对话场景时,模型没做好字符串内部的转义。

4.2 修复方案:字符串层级清理、容错解析库与重试自修复

我不建议直接在代码里写一个加载依赖的第三方库就完事,而是把修复过程分层设计,每一层只解决一层问题。

第一层是字符串清理。不管模型输出了什么,先把返回文本标准化:去掉首尾空白,提取第一个{到最后一个}之间的内容。这一步能解决Markdown围栏和前后解释文本。

第二层是容错解析。字符串清理之后仍可能不是合法JSON。Java生态里我用过json-sanitizer做防御式清洗,它能处理未转义引号、单引号包裹、缺失逗号这类常见问题。Python生态对应的是json-repair库。这两个库都不是万能药,但对于LLM输出场景里的历史错误模式覆盖度很高。

第三层是重试自修复。前两层都失败后,我把解析错误信息直接回传模型:

你的上一次输出未通过JSON解析,错误信息如下: {error_message} 请根据错误信息修正,只输出合法的JSON,不要解释。

这个自修复链路能把绝大多数单次失败在第二轮转成功。需要提醒的是,自修复的Prompt里必须带上明确的错误信息,让模型知道具体哪里不合法,笼统地说“输出不正确”效果很差。

4.3 自修复链路要控制的止损机制

自修复不是无限重试的。我在链路里设置了max_retries=2,两轮修复还没通过校验,这条记录就被标记为EXTRACT_FAILED,进入人工处理队列。

为什么控制在两轮?因为实测数据显示,第一轮自修复成功率在60%左右,第二轮只有15%左右,第三轮基本在5%以下。继续重试不仅浪费Token,而且模型在反复纠错中容易产生新的幻觉,越修越离谱。

另外我记录了一个retry_count字段,这个数据后期很有价值——如果某个团队的消息记录retry_count普遍偏高,说明清洗层的预处理还需要加强,或者该团队的沟通风格需要新增few-shot示例。

5. 批量写入CRM的幂等与去重工程

5.1 字段映射中隐藏类型陷阱

拿到LLM输出并通过校验之后,写入CRM前还有一堆隐蔽的坑。最容易踩的是类型不匹配

LLM输出的budget是字符串“三十万左右”,CRM里的金额字段是Decimal类型,映射层必须先做转换。问题在于“三十万”和“300000”之间不是简单的格式化关系,中文金额单位、口语表达“大概三十多”、逗号分隔符“300,000”都需要专门处理。

日期字段同理。LLM输出“下周三下午”,我需要拿到消息本身的发送时间,计算出具体的日期和时间点。这里有个细节:计算“下周三”时如果发送时间本身就是周三,“下周三”应该理解为下个周三而不是当天,这个判断代码里要写清楚。

还有一个我不小心踩过的地方:CRM里负责人字段存储的是用户ID,而LLM输出的是人名。映射层需要做人名到ID的查询。如果两个销售同名,就要求输入侧清洗时补充“部门”或“区域”信息,否则无法唯一确定。

5.2 写入幂等与重复记录识别

批量写入最怕的不是写入失败,而是同一批数据被重复执行。我们设计了两层幂等保护。

第一层是源消息ID。每条沟通记录在进入管道时就生成了source_message_id,写入CRM时这个ID会写到客户记录或跟进记录的自定义字段上。数据库层面建唯一索引,如果同一条源消息被重复执行写入,数据库直接拒绝。

第二层是业务自然键。有些场景下同一条沟通内容可能通过不同渠道进入管道(比如企微消息和通话转写里都提到了同一件事),源消息ID不同,但业务意义相同。这种情况就需要在CRM侧先做自然键查重,比如“客户名称+大致时间+负责人”能匹配上,就跳过新建,改为更新补充字段。

实测下来,两层幂等把重复数据率从3%左右降到了接近于零。多写这几行代码非常值得。

5.3 批处理失败的部分回滚策略

批量写入时我还有一张处理状态表,记录每条消息在管道中每个环节的状态:

source_message_id | extract_status | write_status | crm_record_id | error_msg | retry_count

写入CRM时以10条为一个批次,逐条执行写入。如果批次中间某几条失败,我不会把整个批次回滚,而是让成功的写入保留,失败的标记为WRITE_FAILED,并根据错误类型决定是立即重试还是转人工。

这么设计的原因很简单:CRM写入大多数失败是单条数据特有的(字段超长、必填缺失、枚举值不合法),回滚整个批次会导致明明成功的部分也丢掉了,得不偿失。而保留成功后,重试时只需要处理失败的那几条,逻辑清晰。

6. 成本与延迟优化:批量场景下的Token控制实测

6.1 Token消耗的大头在哪里

上线一周后我统计了Token消耗,发现一个反直觉的结论:真正的大头不是待抽取的文本,而是提示词本身

一次调用总共消耗Token大约1600个,其中沟通记录正文只有500个左右,系统提示词加few-shot示例占了800多个,输出JSON占300个。也就是说,提示词的固定开销接近单次调用的一半。

如果每天处理3000条记录,单是提示词固定开销就是240万Token。这说明在批量场景里,优化提示词的长度比压缩输入文本更能显著降低成本。

6.2 上下文裁剪与摘要重写策略

我的解决方案是三层裁剪。

第一层是精简系统提示词。把可以用一句话表述的规则用一句话,few-shot从6个压到3个。实测准确率没变,Token却省了40%。

第二层是输入侧长度限制。超过1400字的记录,先用一个小模型做摘要,保留客户、金额、时间、异议点,然后只把摘要传给抽取模型。摘要模型成本更低,做压缩非常划算。这一点和很多人遇到的“上下文越长输出越不稳定”是同一个道理——我在实践里也观察到,超过2000字的上下文会让JSON合法率下降5到8个百分点,所以该截断时就要截断。

第三层是Prompt缓存。系统提示词和few-shot示例是固定不变的,LLM服务商如果支持Prompt缓存,这部分Token每次调用都能命中缓存。我接入缓存后,稳定场景的Token成本下降了近30%。

6.3 缓存、模型分级与并发控制

成本优化的另一条思路是模型分级。短文本(200字以内)、字段明确的高置信度记录走更便宜的小模型;长文本、信息模糊、重试过一次的记录走能力更强的大模型。分级之后综合成本降了25%左右,准确率波动可以忽略。

并发控制方面,我做了两步:一是把批量写入安排在夜间低峰期,二是调用LLM时采用半并发模式——同一时间最多10个并发请求,超出部分排队。原因很简单:LLM接口有速率限制,盲目提高并发只会触发限流,然后陷入“退避-重试-再限流”的恶性循环。用指数退避的策略处理限流异常,实测比固定重试的稳定得多。

7. 踩坑清单与可复用的经验

7.1 我踩过的几个典型坑

第一个坑是字段被补充成了“合理的假数据”。有一次模型把负责人口中的“可能下个月”抽取成了具体日期“2025-06-15”,Schema也通过了,但一看就知道是编的。原因是提示词里没有强调“不能将模糊时间转换为具体时间”。加了这条约束之后,类似问题几乎绝迹。

第二个坑是把金额设计成了字符串。早期版本里金额字段用string存储,后面做商机汇总统计时才发现转换各种格式的字符串有多痛苦。重构成number类型后,虽然解析失败率微微上升,但数据可用性提升巨大。

第三个坑是在校验不到位时就先上线了写入。第一周有几十条脏数据直接写进了生产CRM,虽然靠幂等和标记字段能识别,但清洗历史数据非常费劲。后来我在写入前加了一道强制校验:必填字段缺失、日期格式错误、金额非数字这三种情况一律拦截,不允许绕过。

7.2 可复用的检查清单

检查维度具体检查项建议
提示词是否明确禁止猜测并允许输出null缺失字段必须输出null
提示词是否要求保留原文表述模糊表达保持原样,不要换算
参数Temperature是否设置在0~0.2结构化抽取勿用高温度
解析是否有多层容错(清理词、容错库、自修复)三层缺一不可
校验是否有字段级Schema类型校验必须做类型检查,非仅解析
幂等是否有源消息ID唯一索引必须有
幂等是否有业务自然键查重防止跨渠道重复
映射是否有人名到ID、枚举到编码转换必须做,不能依赖LLM
成本是否精简few-shot并开启缓存提示词固定开销是成本大头
重试是否设置最大重试次数建议2轮,避免无限重试

7.3 如果重新做一次,我会怎么改进

整套系统跑稳定之后,我回过头审视整个方案,最值得改进的是两点。

第一,一开始就引入人工修正反馈闭环。现在抽取失败的记录进入了人工处理队列,但处理结果没有回灌到系统中。如果当时做一个反馈标注界面,让人工修正后的数据成为few-shot示例的候选池,模型准确率会随运行时间持续提升。

第二,把中间Schema设计得再薄一点。当前中间层有部分字段是从CRM字段直接复制过来的,导致Schema里出现了几个不太合理的耦合字段。如果重新设计,我会先和CRM团队一起梳理字段血缘,再定Schema,避免后期来回改提示词。

我在实际运行中还有一个体会:这类系统的核心不在于模型有多强,而在于工程结构能否把模型的不可控性限制在一个小范围内。提示词控制格式,校验器拦截非法输出,映射层做确定性转换,幂等机制保证数据不脏——每一层都解决一个具体问题,最终整个管道才是可依赖的。如果你也在搭类似的抽取管道,希望这篇实践记录能让你少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 20:21:15

用15个GitHub开源项目替代付费软件,一年省下3000元

前阵子整理自己收藏夹里的GitHub项目时,我突然意识到一件事:过去一年我买过、续费过的那些付费软件,几乎每一个都能在GitHub上找到能打的免费替代品。我把15个我实际用过的、星标高、维护很活跃的开源项目捡出来,挨个替换掉手里的…

作者头像 李华
网站建设 2026/9/19 20:20:09

ZenCart多地区多运费配置:Zone定义与Zone Rates实战

简介:ZenCart作为开源电商平台,多地区多运费配置是跨境运营的关键环节。这份Word文档面向店铺管理员和开发人员,系统讲解基于分区(shp1、shp2)的国家指定、阶梯重量运费规则(如0.5:20,1:30,2:40&#xff09…

作者头像 李华
网站建设 2026/9/19 20:19:34

出租车计费计课程设计:从脉冲计数到Verilog仿真与TTL实现

简介:出租车计费计数字电路课程设计文档是一份面向电子信息工程及相关专业学生的完整课设参考,围绕行车里程计费、等候时间计费和起步费三部分,给出总额不超过99.99元的计费器设计方案,内容涵盖设计目的、总体框图、各单元电路详解…

作者头像 李华