LLM结构化提取实践:从非结构化沟通记录到CRM的工程落地
我们团队年初接到一个挺头疼的需求销售、客服每天的沟通记录散落在企业微信、邮件、通话录音转写文本里格式五花八门想把这些信息沉淀进 CRM只能靠人工一条条整理。量大、重复、容易漏销售不愿录管理层又觉得数据不准。后来我们用 LLM 做结构化提取再走批量管道写入 CRM算是把这条链路彻底盘活了。这篇文章把整个工程实践从方案选型、数据建模、提示词设计到稳定性保障讲透适合正在做类似非结构化数据治理或者LLM 落地业务系统的团队参考。1. 非结构化沟通记录的结构化难点在哪1.1 看似简单实际要命的数据形态先看一个真实例子。销售和企业微信客户的一段沟通记录可能是这样的张总您上周看的那个企业版方案报价 38800 那个我跟领导申请了一下可以给您做到 34000含首年服务费。如果这周能定我还能帮您多争取两次上门培训。另外您说要考虑的那个增购模块得看具体账号数我明天给您报个价。这段话信息密度很高但机器直接读不出来。人的大脑能快速理解客户是谁张总、聊了哪个产品企业版方案、报价34000原价 38800、条款含首年服务费、两次上门培训、后续动作明天报价增购模块。但这些信息落到传统规则引擎手里基本就是一堆无标点的字符串。传统做法一般是正则匹配加关键词提取写一堆规则去匹配报价价格元万这类词再结合上下文猜。问题是售前沟通的语言变化太丰富有人发三万四、有人发34000、有人发3.4 个 W正则规则写得越多维护成本越高误报漏报反而越严重。1.2 为什么 LLM 适合干这件事LLM 真正的优势不是聪明而是它理解语义而不是匹配字符。它能从一段看似松散的话里精准提炼出结构化字段而且能处理同义表达、指代消解、跨句关联。比如上文您上周看的那个隐含了产品是企业版方案的指代关系含首年服务费能归属到价格条款字段两次上门培训能归属到增值约定字段。关键是LLM 输出可以约束成结构化格式JSON 或 XML这就是结构化提取的工程基础。只要向模型定义好目标结构并给出清晰的提取指令模型就能把自由文本映射成字段化数据。我们用此类方式处理几千条历史聊天记录字段完整率平均在 92% 以上这在正则方案下很难想象。不过也要泼一盆冷水LLM 提取不是万能的。它擅长语义理解和信息归类但不擅长精确计算、也不擅长处理你看着办这类隐含意图。所以在工程上我们要做的是把 LLM 放在信息提取这个环节把确定性计算和校验交给传统代码。这其实是 LLM 落地业务系统的一个通用原则让大模型做它擅长的事让规则引擎做确定性的事。2. 结构化提取的方案选型直接调用、Function Calling 还是微调2.1 三种常见技术路线对比在确定了用 LLM 做提取这个方向后我们面临一条分岔路用哪种方式让模型输出结构化结果方案实现方式优点缺点适用场景纯 Prompt JSON 指令在提示词中要求模型返回 JSON并给出示例实现最快兼容所有模型输出格式不稳定偶尔会多出解释文字或 markdown 代码块快速验证、内部工具Function Calling / JSON Mode调用模型的原生函数调用能力或结构化输出模式输出格式有模型层保证稳定性高需使用支持该能力的模型逻辑上需要多做一层约束生产环境首选微调Fine-tuning针对特定业务场景训练专属模型准确率上限最高推理成本可降低需要构建标注数据集、训练成本高、迭代周期长场景高度固定、语料量大且规范我们最终选了Function Calling / JSON Mode这条路线。原因很直接团队需要快速上线而且模型的函数调用能力已经在底层做了格式规整输出 JSON 的失败率比纯 Prompt 方案低一个数量级。实测下来纯 Prompt 方案大约有 5%-8% 的请求返回格式不合法而启用 JSON Mode 之后这个比例降到 1% 以下剩余的绝大多数是字段遗漏而非格式错误。2.2 结构化输出协议的设计在 Function Calling 模式下我们需要向模型声明一个函数告诉它参数有哪些、类型是什么、必填还是选填。这一步相当于给模型一张信息提取的表格框架。我们定义了一个名为extract_communication_info的函数参数大致如下{ name: extract_communication_info, description: 从沟通记录中提取结构化客户信息, parameters: { type: object, properties: { customer_name: { type: string, description: 客户姓名或企业名称 }, contact_channel: { type: string, enum: [wechat, email, phone, meeting] }, products: { type: array, items: { type: object, properties: { product_name: { type: string }, quantity: { type: integer, description: 数量未知时填空 }, unit_price: { type: number, description: 单价单位为元 } } } }, follow_up: { type: object, properties: { action: { type: string }, due_date: { type: string, format: date } } }, summary: { type: string, description: 20字以内的沟通摘要 } }, required: [customer_name, contact_channel, summary] } }这里的关键不是字段多而是把必填项压到最少。模型对模糊信息的处理策略是倾向于宁可漏掉也不敢编造如果你把unit_price设为必填而原文里恰好没提价格模型可能会从上下文去猜一个数字——这是绝对不能接受的。所以我们的原则是只有那些几乎每次都出现且不会出错的信息才设为必填其余字段一律选填缺省值用 null 或空字符串标记。2.3 一次预处理少走很多弯路一个容易被忽略的细节在调用 LLM 之前先对原始文本做一次轻量预处理能显著提升提取质量。比如把聊天记录里的系统消息剔除把多条引用的历史消息折叠成一条上下文把语音转写文本里的口误做简单清洗。语音转写文本的清洗尤其重要。我们接的是某云厂商的转写结果三千八和3800经常混着出现还会带出嗯那个这类语气词。我们在预处理阶段做了一件事把中文数字替换成阿拉伯数字并删除高频语气词。这一步不复杂但能让模型少受很多干扰提取准确率大概能提升 3-5 个百分点。预处理还有一个作用是控制输入长度。有些沟通记录可能非常长单次对话上万字直接塞进上下文既浪费 token又可能让模型在长文本中忽略关键字段。我们会先做一次摘要压缩如果文本超过 3000 字先用一次独立的 LLM 调用把长对话摘要成 800 字以内的要点再做结构化提取。这里的策略是先摘要、后提取两次调用的效果远好于一次调用直接提取长文本。3. 数据建模与跨记录归一化比提取更考验功力的部分3.1 字段建模的几个教训结构化提取只是第一步更麻烦的是把这些字段映射到 CRM 的数据模型里。我们接的 CRM 是企业内部的定制系统它的客户表、产品表、跟进记录表各有自己的字段约束。比如 CRM 里的金额字段要求必须是 decimal(10,2)但 LLM 可能输出34000或者三万四CRM 里的客户名称字段要求唯一但 LLM 可能从聊天记录里提取出张总而不是张伟这就导致关联失败。我们最开始的做法是让 LLM 一次性输出 CRM 想要的字段后来发现这条路走不通。原因是 CRM 字段和沟通记录之间并不是一一对应的直接让模型去填 CRM 的字段等于强迫它去理解 CRM 的业务逻辑这对模型来说不是它擅长的事。正确的做法是做一层中间转换。LLM 只负责输出一个与业务逻辑解耦的通用沟通事实层字段定义尽量贴近自然语言语义再由代码转换成 CRM 的字段格式。中间的属性映射、格式校验、归一化由传统代码完成。这样职责边界清晰模型负责语义理解代码负责数据清洗和转换。3.2 实体归一化的实现策略实体归一化是跨记录稳定性最关键的环节。同一个客户销售在聊天里可能被称为张总、张伟、老张、zhangweixx.com我们要把这些都关联到同一个 CRM 客户 ID 上。如果这一步做不好不仅会创建大量重复客户还会让后边的数据分析和销售漏斗完全失真。我们的归一化策略分三层规则层先按精确匹配做一次查找比如手机号、邮箱、企业域名这些强标识。这一步能解决大部分问题而且不需要大模型参与。LLM 归一化层对规则层没有命中的记录构造一个归一化请求把待匹配的客户名 候选客户列表一起发给模型让模型判断是否属于同一个人。这一步准确率很高因为模型的语义理解能力能识别张总和张伟之间的指代关系。人工兜底层归一化置信度低于阈值我们设为 0.75的记录进入人工审核队列不自动创建新客户。这个三层策略上线后重复客户创建率降低了 80%。一个很典型的场景是销售在电话里说我这边跟老王那边聊得差不多了规则层匹配不到任何客户老王这两个字也没法通过关键词找全名。但 LLM 归一化层能够结合对话上下文推断老王可能指的是上一轮对话里提到的王建国只要候选列表里有这个名字模型就能给出一个较高的匹配分数。3.3 去重与幂等设计的工程细节批量写入 CRM 场景下最怕的是同一段沟通记录被重复提取、重复写入。比如某销售把一段聊天记录转发到了两个群两个群的机器人都会触发提取逻辑如果不做幂等CRM 里就会出现两条一模一样的跟进记录。我们的方案是给每条原始沟通记录生成一个唯一的业务键wechat_message_id或者email_message_id。提取流程开始前先查一次 CRM 是否存在该业务键的记录存在就直接跳过。但这个方案有个盲区如果原始记录本身不是唯一的比如同一个 PDF 被上传了两次但每次生成的消息 ID 不同就需要用到语义指纹。语义指纹的做法是对原始文本取一个固定的哈希子串比如先做一次规范化去空格、统一大小写、去除无意义标点然后计算 MD5 的前 16 位作为指纹。每次写入前先去重表查询指纹如果命中说明这条记录已经被处理过了。这套方案上线后重复写入问题基本清零。4. 批量管道的架构设计与性能调优4.1 总体流程从消息接入到 CRM 落库整个系统的数据流并不复杂但中间的可靠性保障和调度逻辑比较考究。我们的管道分为四个阶段接入层监听 webhook企业微信的群消息、邮件服务器、通话转写平台把原始消息推入消息队列。预处理层从消息队列消费数据做清洗、数字归一化、长文本压缩。LLM 提取层调用大模型接口做结构化提取并把结果写入中间结果表。转换与写入层从中间结果表读取数据做字段映射、归一化、去重校验最终批量写入 CRM。这种拆分看起来多绕了一圈中间结果表但实际好处非常大。因为 LLM 调用是耗时操作而且失败率比普通 API 调用高不少如果直接一步到位从接收到 CRM 写入一旦模型调用失败就得整体重来。有了中间结果表我们可以把已提取但未写入 CRM的数据缓存住CRM 写入失败时只需要从中间表重放不需要再调一次大模型省了 token 也省了时间。4.2 批次大小的选择不是越大越好批量写入 CRM 时一个常见误区是以为批次越大、性能越高。实际上绝大多数 CRM 系统的 API 都有单次请求体大小限制而且单条记录失败会导致整批回滚或者部分成功——这取决于 CRM 的实现机制。如果我们用的 CRM 支持分批部分成功那批次大一点问题不大但如果不支持批次太大反而会因为某一条脏数据拖累整批。我们最终把批次大小定为50 条/批并通过压测验证了这个值的合理性。以一次拉取 1 万条记录为例200 个批次在并发数为 8 的情况下整体耗时在 2 分钟以内如果批次增大到 200单批失败率明显上升重试成本也随之增加。综合下来 50 是一个在吞吐和稳定性之间比较均衡的值。4.3 异步任务编排与失败重试的细节由于 LLM 调用耗时较长平均 3-5 秒整个流程必须设计成异步任务。我们用了一个简单的状态机来跟踪每条任务的状态PENDING → PROCESSING → EXTRACTED → TRANSFORMED → WRITTEN → SUCCESS任何一个环节失败都会进入FAILED状态并触发有限次数重试。这里有一个很容易踩的坑重试策略不能是无脑全量重试。如果某条数据卡在CRM 写入失败环节可能是因为 CRM 系统本身在维护也可能是这条数据本身有问题比如字段超出了 CRM 允许的长度。无脑重试不仅浪费资源还会把问题数据无限重放挤压正常数据的处理资源。我们的做法是区分失败类型可重试失败超时、速率限制、网络波动、CRM 临时不可用。这类错误设置重试次数为 3 次采用指数退避策略1 分钟、5 分钟、15 分钟。不可重试失败字段校验失败、CRM 返回明确的业务错误如客户 ID 不存在、LLM 输出连续几次解析失败。这类错误直接标记为NEED_MANUAL_REVIEW推送到人工处理队列不再自动重试。这个区分逻辑极大减轻了后续人工排障的负担。以前所有失败都混在一起运维人员每天要看一堆相似错误现在只有真正需要人介入的数据才会出现在人工队列里一周处理量从几百条降到了二三十条。4.4 速率控制与 token 成本控制LLM 调用是有成本上限的如果管道吞吐设计得过高直接拉满模型服务的 QPS账单可能会在一天内超出整个月的预算。我们在接入层做了一级限流单位时间窗口内最多触发 N 次 LLM 调用并且把不同来源的消息按优先级排队。一个更细的成本控制策略是重复内容去重后再调模型。实践中我们发现同一个客户在一天内可能会收到多条几乎一样的群发消息如果每条都调一次模型提取纯属浪费。我们在预处理层增加了一个短文本去重逻辑如果消息文本和过去 24 小时内处理过的记录相似度超过 95%直接沿用上次的提取结果不再触发模型调用。这块节省的 token 成本大概在 15% 左右效果比较可观。5. 提示词设计与结构化校验精调提取精度的核心5.1 提示词的结构细节如果说架构是系统的骨架提示词就是提取精度的灵魂。我们经过多轮迭代最终沉淀了一套比较稳定的提示词模板核心结构包含四个部分角色设定告知模型自己是CRM 客户信息提取助手明确只做信息抽取不做推理和评价。输入说明说明输入是沟通记录可能包含口语化表达、错别字和无意义语气词要求忽略噪音。输出约束说明必须输出 JSON字段必须与给定 schema 一致未知信息用 null 填充禁止编造。再思考要求要求模型在提取之前先判断哪些信息是明确说出来的哪些是推测的并只提取明确信息。听起来很玄但第四点再思考要求是最有效的。拿之前那个例子来说如果模型明确知道只有明确说出的信息才可以写入它就不会在客户没提预算时猜一个数字。而如果没有这条约束模型往往会为了有用而脑补。我们进一步用了一个小技巧让模型先输出一个思考字段再输出最终提取结果。比如在提取之前模型会生成一段待提取事实列表把这轮对话中明确提到的关键事实逐条列出来再基于这份列表填充结构化 JSON。虽然会增加一些 token 消耗但提取准确率比直接一步到位高出 5 个百分点左右。这个技巧的原理说起来也不复杂它相当于让模型先做一次信息梳理再基于梳理结果做信息编码相当于加了一道中间校验。5.2 结构化输出的后置校验JSON Schema 业务规则双重把关模型输出的 JSON 即便格式合法也不代表内容就一定是合规的。所以我们加了两层校验第一层是 JSON Schema 校验用程序把模型输出与预定义的结构化协议做比对检查字段类型、必填项、枚举值是否符合预期。第二层是业务规则校验这一层完全是代码写的包括但不限于金额不能为负数日期格式必须是YYYY-MM-DD且不能早于今天太多防止模型把去年提取成奇怪的年份客户名称长度不能超过 CRM 字段限制比如 50 个字符联系方式必须是合法的手机号或邮箱格式。这两层校验跑在 LLM 调用之后、CRM 写入之前。任何一层失败数据都不会进入 CRM而是打回预处理队列。比较关键的是业务规则校验的结果会反馈给 LLM 调用参数——如果某类数据频繁因为同一类规则失败比如日期格式经常错我们会针对性修改提示词把规则直接写进提示词的输出约束部分。这也是一个持续调优的迭代流程。5.3 多轮迭代与回归测试集这里要特别提一下回归测试集的工程价值。我们每次修改提示词或调整结构化协议都不会直接上生产而是先跑一遍预先收集的 300 条典型测试集对比新旧版本的提取结果。这个测试集覆盖了各种边界情况高价订单、含折扣、多产品并行讨论、客户婉拒、明确约定下一步时间等。只要跑完回归测试就能清楚地看到修改是提升了整体精度还是只修好一个 bug 却弄坏了另一个功能。跑回归测试的时候我们还会顺带做一次置信度校准。对于每条提取结果我们让模型输出一个置信度分数0-1然后统计不同置信度区间下的准确率。比如置信度在 0.9 以上的记录准确率在 98%但置信度在 0.6 左右的记录准确率只有 70%。有了这个统计我们就可以设定一个自动写入阈值低于该阈值的记录自动转人工审核从而在自动化率和准确率之间取一个平衡点。6. 稳定性保障与常见事故复盘6.1 上游接口故障的降级策略LLM 服务再稳定也不可能保证 100% 可用。我们遇到过两次上游模型服务连续半小时返回 5xx 的情况如果管道不对这种故障做处理所有消息会一直堆积在队列里消息队列本身就可能因为积压过多而宕机。我们的降级策略是三级降级第一级限流降级检测到上游连续多次失败后自动把并发数降到原来的 20%同时把触发 LLM 调用的阈值调高只处理高优先级消息。第二级缓存降级查询是否有相似消息的历史提取结果优先复用减少上游调用量。第三级人工转写如果 LLM 服务不可用时间超过 15 分钟启用人工处理队列保证重要消息比如有意向的客户不因为系统故障而丢失。每一级降级都是可独立配置的运维人员可以在配置中心动态调整不需要改代码重新发布。6.2 数据错乱事故LLM 输出的日期和实际日期不一致这个事故挺有代表性。我们曾发现一条跟进记录的处理时间被写成2035-08-12明显是模型搞错了。排查后发现问题出在提示词中的日期处理指令不够明确——模型有时候会把对话里提到的下个月理解成当前时间对应的月份但如果对话文本里恰好有明年之类的词模型就会生成一个不存在的年份。修复方式是在提示词的输出约束中加入明确规则提取日期时必须基于当前系统日期2025-xx-xx进行换算如果原文提到的是相对时间如下周五请计算为实际日期无法确定时输出 null。同时在后端增加日期范围校验比如不允许设置超过当前日期 2 年以上的日期双管齐下后这类问题基本没有再次出现。6.3 脏数据对 CRM 主数据的影响最后一个提醒结构化提取的最终目标不是模型跑得通而是写入 CRM 的数据干净可分析。如果 CRM 主数据本身很脏那你的提取精度再高也白搭。比如 CRM 里的产品列表可能和实际销售的产品不完全一致LLM 提取出的产品名虽然语义正确但 CRM 里没有完全匹配的选项这时候我们要做的不是让模型输出任意自由文本而是限定枚举值。我们在结构化协议中把product_name改成枚举类型枚举值从 CRM 产品表中动态加载并拼进提示词让模型只从给定列表中选择。这种做法有两个好处一是提取结果天然满足 CRM 的数据约束二是模型的选择准确率比开放式提取高很多因为候选集缩小、范围限定它不需要猜测产品名是否写对。当然这种方式也有代价当 CRM 产品表特别大比如几千个 SKU时把全部枚举值塞进提示词会显著增加 token 消耗。我们的处理方式是先做一次粗分类让模型先在几个大类中选再根据大类加载对应的小类枚举列表分两次提取解决这个问题。7. 多语言与多来源沟通记录的扩展处理这块算是我们上线后逐步优化出来的一个隐藏需求但我觉得值得单独拿出来讲。很多团队的沟通记录不只有中文。外资客户的邮件往往是英文国内销售和客户用中文聊而合同相关的书面语可能夹杂法务条款。如果只对中文做结构提取英文邮件就完全靠人工处理了这会形成新的瓶颈。我们在多语言处理上采用的是语言感知预处理先用一个轻量语言检测模型判断记录的主要语言然后针对不同语言加载不同的提示词模板和交付模板。核心的提取逻辑是同一套统一的结构化协议但提示词中的角色描述、输入说明和示例会切换成对应语言。有一个细节值得注意多语言场景下实体归一化的难度明显增加。比如一位客户在中文记录里叫张伟但在英文邮件里落款是Steven Zhang两边的记录要关联到同一个客户 ID就得靠 LLM 归一化层去理解两个名字之间的映射关系。我们在候选匹配阶段会把 CRM 客户表中所有可能的英文名和中文名都拉出来作为候选再让模型判断是否匹配。这一步效果不错但还是建议保留一个人工审核兜底因为建错客户关联比少建一条跟进记录要难处理得多。另外多来源微信、邮件、电话的记录在写入 CRM 时应该保留来源字段这个字段不仅是为了追溯也是后续数据分析的一个维度——比如可以统计通过电话沟通后的成单率是不是比微信高。这类统计有了准确的结构化数据之后对销售运营的指导价值很大。如果团队业务有多国客户或经常切换语言建议在一开始建模时就预留语言字段和多个名称字段法定名称、常用名称、英文名称避免后面数据模型推倒重来。8. 管道上线后的持续迭代从准确率到业务价值系统上线只是开始后面的持续迭代才是真正拉开差距的地方。我们每两周会做一次提取质量复盘方式是抽取最近两周的 500 条真实写入记录人工标注后对比模型提取结果统计字段准确率和完整率。这个数字能直观反映模型是否因为某类新业务模式比如新产品线、新促销活动而出现准确率滑坡。除了准确率我们还关注一个更贴近业务的价值指标结构化数据对 CRM 使用率的拉动。过去销售要手动录入跟进记录很多人在拜访完客户之后嫌麻烦就不录了CRM 里的数据永远停留在当初销售想让你看到的那些。现在有了自动提取系统里能看到每一次沟通的完整脉络管理层的销售漏斗分析终于有了数据基础。这个变化让团队对 LLM 提取方案的认可度非常高。迭代过程中我们也总结出几个比较通用的优化方向Prompt 版本化提示词修改要像代码一样有版本记录能回溯能对比最好配套回归测试集。抽取字段的动态扩展业务部门随时可能提出新的信息维度比如客户预算范围竞品提及情况系统设计时要预留字段扩展的灵活性。数据回刷能力当提示词优化后历史数据是否值得重新提取一次我们开发了一个按时间段、按客户维度的回刷工具可以在夜间低峰期对存量记录做重提取确保所有历史数据也适用最新的提取逻辑。我个人的体会是LLM 结构化提取写进 CRM 这个实践本质上不是模型调优的问题而是一个完整的数据工程问题。想清楚每个环节的边界让模型负责语义理解、代码负责确定性的校验与转换、人工兜底处理低置信度的边缘案例这套系统才能真正从 Demo 走向稳定运行。希望这份实践记录能帮那些正在评估或落地类似场景的团队节省一些趟坑的时间。