ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LLM智能用户画像:非结构化文本驱动的动态标签生成与增量更新实践

LLM智能用户画像:非结构化文本驱动的动态标签生成与增量更新实践 简介一套基于大语言模型的智能用户画像分析系统完整参考实现面向企业营销、用户运营与数据分析从业者聚焦利用LLM整合行为分析、情感识别、消费习惯挖掘、价值观推断等能力构建动态标签与多源融合画像适用于客户洞察、精准推荐和个性化服务等场景。压缩包内共28个文件、约144KB以16个Python源码文件为主体覆盖接口调用、数据预处理与核心模型模块另含3个Markdown文档、1个Docx说明及依赖锁定等配置便于理解项目结构和快速运行调试。目前已有282人学习说明这一方案在实践中有一定参考价值。读者可参照源码与流程文档掌握从多源数据清洗、大模型语义分析到动态标签生成与画像输出的完整链路也可直接修改脚本应用于自身用户数据场景或作为课程设计与企业项目的起点。1. LLM智能用户画像当非结构化文本第一次能被直接算成标签做用户画像做了几年最头疼的不是模型效果而是数据根本喂不进模型。传统画像系统依赖结构化字段性别、年龄、消费金额、浏览频次。但真正决定用户决策的是客服对话里的抱怨、评论区的情绪、问卷里那句“我就喜欢小众的东西”这类非结构化文本。这些数据占比越来越高却始终被挡在特征工程之外。这个项目做得最扎实的一件事是把大语言模型LLM放进了用户画像的主链路不是做Demo而是把多源数据融合、行为序列建模、情感识别、价值观推断、动态标签生成这些环节全部串起来让自然语言处理能力直接服务于标签产出。对正在做精细化运营、用户增长、CRM系统升级的团队来说这是少有的能直接对齐业务场景的参考实现。我们接下来拆开看它每一层是怎么搭的。2. 系统架构与核心模块从数据接入到标签产出的完整链路2.1 多源数据融合结构化与非结构化数据怎么进同一套管道这个系统在数据接入层的设计逻辑很清楚结构化数据走常规批处理通道非结构化数据先做清洗和落库再统一进LLM处理管道。作者没有强调“AI自动处理一切”而是老老实实做了数据分层。数据融合这块有个关键细节它把行为事件流和用户静态属性分开存储。静态属性走MySQL或数仓维度表行为事件流走Kafka或日志系统。在融合阶段静态属性和实时行为通过user_id进行拼接。非结构化文本则单独建了一张文本数据表每条记录附带来源渠道、时间戳和业务类型三个关键字段。我一般会建议在融合层加一个“数据新鲜度”的字段配置因为LLM处理文本是有延迟的如果用户在App上的实时行为和NLP标签到达时间错位超过一定阈值后续的画像准确性会明显下降。这个项目虽然没有在代码里直接写这个控制逻辑但从数据表的字段设计来看它预留了数据版本管理的位置接入方可以自行扩展。2.2 标签体系设计动态标签怎么做到既可解释又能更新这个项目的标签体系没有用“黑匣子式”的Embedding直接顶上去而是保留了传统标签体系的层级结构在叶子节点上做了动态化。具体来说一级标签是用户基础属性二级是行为偏好三级是消费能力、情感倾向、价值观倾向每一级标签都有对应的文本证据链。这正好是LLM类用户画像项目和传统标签系统最本质的差别——传统系统产出标签是“统计结果”这个系统产出标签是“带证据的结论”。比如“价格敏感”这个标签传统系统可能只记录一个布尔值而LLM系统会同时存储触发该标签的原始文本片段、置信度评分和更新时间。动态更新机制上项目采用的是“事件触发定时全量”的双轨策略。当用户产生新的行为事件或文本内容时系统对关联标签做增量重算只更新受影响的那几个标签不需要全量跑一遍。同时每天凌晨做一次全量画像刷新用来纠正增量计算可能带来的偏差。这部分在实际业务中你会感受到明显差别触发式更新保证实时性定时全量保证稳定性。2.3 LLM在画像系统中的角色定位它到底负责哪一层这里要明确一个容易混淆的点LLM不是替代了整个统计模型体系而是只负责语义理解和标签推断这两层。频次统计、金额分布、时间衰减这些计算仍然走传统的统计模型跑得快、解释性强。LLM只处理那些“非它不可”的任务识别文本中表达的态度、推断消费偏好背后的动机、理解上下文中的隐含需求。这种分工的价值在实践中非常明显。统计模型处理结构化数据时延迟极低每天处理几千万条行为日志没有压力。LLM处理文本的成本和延迟都高但如果只处理清洗后的短文本片段控制在几千条量级成本完全可控。这种轻量级接入比把所有数据都塞给LLM要实用得多。3. 用户行为与消费习惯挖掘从事件序列到偏好标签的实现路径3.1 行为序列建模怎么把时间线压缩成LLM能理解的输入行为序列是用户画像的核心输入之一把这个序列直接全部塞给LLM显然不现实所以项目采用了一个非常实用的做法——先模式化、后语义化。也就是说先用传统方法把用户最近N次行为压缩成结构化特征再把这些特征翻译成LLM能理解的文本描述。def build_behavior_sequence(user_events, max_len20): 将用户行为事件序列压缩为带权重的结构化字典 user_events: 按时间升序排列的事件列表 max_len: 最多保留的行为条数 # 只保留最近 max_len 条事件避免序列过长 recent_events user_events[-max_len:] category_counter {} time_distribution {} for evt in recent_events: # 行为类型按业务大类聚合比如“浏览商品详情”和“查看评价” # 统一归入“商品研究”这个大类再细分子类型 main_cat evt[event_type].split(_)[0] sub_cat evt[event_type] category_counter[main_cat] category_counter.get(main_cat, 0) 1 # hour_bucket 把一天切成四个时间段捕捉消费时间偏好 hour_bucket evt[ts] // 6 time_distribution[hour_bucket] time_distribution.get(hour_bucket, 0) 1 # 计算最近7天内行为占比和品类集中度作为输出特征 recent_ratio len(recent_events) / max(len(user_events), 1) top_cat max(category_counter.items(), keylambda x: x[1]) return { top_category: top_cat[0], category_cnt: len(category_counter), recent_ratio: round(recent_ratio, 2), time_slots: time_distribution, total_events: len(recent_events), }这里有个细节值得注意hour_bucket evt[ts] // 6这行是把小时数直接整除6得到0到3四个时段段位。很多团队做时间偏好时用的是hour字段直接做one-hot但那样维度太散LLM在处理这种稀疏时间特征时反而不如聚合后的时段稳定。压缩到四个时段后你给LLM的Prompt里可以直接描述“该用户偏好凌晨时段活跃”模型的理解准确率会明显更高。3.2 消费能力与习惯推断不依赖支付金额的替代方案支付金额在很多业务场景下是拿不到的尤其是内容平台、社区型产品、B2B服务。项目里用的是代理特征方案从用户浏览的商品价格带分布、购买频次、客单价的区间分布、加购但未支付的比例来推断消费能力。这四个代理特征叠加后基本能还原出用户的消费层级。def infer_consumption_level(price_list, purchase_count, cart_rate): 根据用户行为代理特征推断消费水平 price_list: 用户浏览或购买商品的价格列表 purchase_count: 近30天购买次数 cart_rate: 加购未支付比例 import numpy as np if not price_list: return {level: unknown, confidence: 0.0} avg_price np.mean(price_list) price_std np.std(price_list) # price_std 反映价格选择的离散度离散度高说明用户 # 对价格不敏感只挑喜欢的买离散度低说明有价格限制 dispersion_score min(price_std / (avg_price 1e-6), 1.0) # 购买频次折算成月度活跃度低于2次视为低频 freq_score min(purchase_count / 5.0, 1.0) # 加购未支付比例高说明用户价格敏感比来比去不下单 cart_sensitivity min(cart_rate, 1.0) level_scores { high: 0.6 * dispersion_score 0.3 * freq_score - 0.2 * cart_sensitivity, mid: 0.3 * dispersion_score 0.5 * freq_score 0.1 * cart_sensitivity, low: 0.1 * dispersion_score - 0.1 * freq_score 0.6 * cart_sensitivity, } best_level max(level_scores, keylevel_scores.get) confidence level_scores[best_level] return { level: best_level, confidence: round(float(confidence), 3), feature_note: favg_price{avg_price:.1f}, purchase_count{purchase_count}, cart_rate{cart_rate:.2f} }注意这个方案里的dispersion_score这个特征在实际效果上非常灵。高消费用户表现出的典型特征是价格离散度大——他可能买一瓶3块钱的矿泉水也买3000块的耳机他不会因为价格低就不买也不会因为价格高就放弃完全按需求来。低消费用户则相反浏览价格高度集中在一个窄带里。这个特征比简单用均值来判断要准得多。3.3 行为特征与LLM结合的边界哪些推断交给模型更合适行为序列压缩出来的特征适合做规则式推断因为它本质上是统计结果。但有些东西统计不出来比如用户访问了三款竞品后说了一句“还是觉得你们家的设计更顺眼”这是明显的品牌倾向信号普通特征工程无从下手。这类推断一定要交给LLM做。项目里的做法是把结构化特征作为“上下文”把非结构化文本作为“证据”组合成Prompt输入给模型。结构化特征负责划定候选标签范围LLM负责从文本中确认或推翻候选标签并补充新的标签。这个流程既保持了统计模型的稳定性又发挥了LLM的语义理解优势比两者分开用的效果要好很多。实际落地时要注意一个顺序问题先跑统计特征再跑LLM推断。如果顺序反了先让LLM过一遍全量文本再把产出结果和统计特征拼接那成本会增加一大截但准确率几乎不会提升。因为LLM只处理“需要理解”的那部分文本就够了行为统计压根不需要它插手。4. 情感识别与价值观推断用提示词约束LLM输出画像标签4.1 情感识别从评论和客服会话中提取跨渠道态度标签情感识别模块在这个项目里不是简单地输出“正面/负面/中性”而是要求模型标注情感强度和具体对象。用户说“物流快得离谱”和“物流还行”都是正面但强度完全不同在画像系统里对应的标签权重也完全不同。这里的关键在于输出Schema的定义项目采用的是一个两层结构粗粒度情感方向加细粒度对象标签。{ sentiment_analysis: { direction: positive|negative|neutral|mixed, target: price|logistics|product_quality|service_attitude|brand_image, intensity: 0.0, evidence: 原文关键句, trigger_words: [快得离谱, 客服] } }这个Schema设计得好的一点是target字段它把情感绑定到了具体业务对象上。用户抱怨“客服回复慢”这属于服务态度问题不代表对产品本身不满意用户写“东西还行但快递太慢”这就是混合情感方向是mixed。如果只记录一个整体情感分数后续做服务改进时完全无法定位问题。实际使用中trigger_words这个字段对画像系统的可解释性帮助很大。它相当于给业务团队提供了一个由模型自动抽取的“关键词证据”运营人员不需要去翻原始文本只看这个字段就能明白为什么用户被打上这个标签。4.2 价值观推断大模型文本推断与传统心理测量量表的互补关系项目里价值观推断是外界最容易误解的部分——它不是在用什么心理学理论模型而是用LLM直接从用户的文本表达中归纳价值取向。比如一个用户经常写“性价比才是王道”“够用就行”和另一个用户写“要买就买最好的不要将就”这两段文本对应的消费价值观截然不同传统画像系统根本不可能从这种短文本里提取出这个维度。def infer_value_orientation(text_snippets, model_client): 从多条文本片段中推断用户价值观倾向 text_snippets: 用户近90天最有信息量的文本片段列表 model_client: 兼容 openai.ChatCompletion 接口的客户端 prompt f 请从以下用户发言片段中推断消费价值观取向。 只输出JSON不要额外解释。 用户发言片段 {chr(10).join(text_snippets[:8])} 输出JSON格式 {{ orientation: 品牌优先|性价比优先|体验优先|实用优先|择优主义, support_snippet: 最能体现该取向的一句原文, confidence: 0.0 }} 要求 1. 如果多段文本体现不同取向取最明显的那一个 2. support_snippet 必须原样引用用户原文 3. 无法判断时 orientation 输出 unknownconfidence 为 0 client model_client response client.chat.completions.create( modelllm_provider_model, messages[{role: user, content: prompt}], temperature0.2, response_format{type: json_object}, ) import json result json.loads(response.choices[0].message.content) return result代码里temperature0.2是刻意调低的值画像是判断型任务不是创意型任务温度太高会让模型文体不稳定同一批文本两次跑出的价值观标签可能不一致。response_formatjson_object则是强制输出JSON结构避免模型在回答末尾附带额外解释这在批量处理时省掉很多解析麻烦。这里踩过一个坑有的模型服务端对response_format参数支持不稳定如果你本地跑这个代码报错先确认模型API版本是否支持JSON Mode不支持就去掉这个参数在Prompt里再强调一次“只输出JSON”也能有八成效果。4.3 结构化输出解析模型输出不按Schema走怎么办LLM输出标签的稳定性永远是这类项目落地的最大摩擦面。最常见的现象是模型在90%的情况下都乖乖输出JSON但偶尔会在JSON后加一行“以上是我对用户价值观的分析”然后你的json.loads就直接抛异常。def safe_parse_llm_output(raw_text): 容错解析LLM输出优先提取JSON部分 import json, re # 先尝试直接解析 try: return json.loads(raw_text) except json.JSONDecodeError: pass # 直接解析失败时尝试从原文中截取第一个 { 到最后一个 } 的部分 start raw_text.find({) end raw_text.rfind(}) if start ! -1 and end ! -1 and end start: json_str raw_text[start:end1] try: return json.loads(json_str) except json.JSONDecodeError: pass # 如果连花括号都不完整只能把这条样本标记为无效 return {status: parse_failed, raw_output: raw_text[:200]}这个函数建议直接抄下来用。在生产环境里即使你做了JSON Mode约束依然会有一小部分输出格式异常安全解析函数是必需品不是可选项。注意json_str raw_text[start:end1]这段是从第一个左花括号取到最后一个右花括号中间的文本理论上必须是一段合法JSON如果模型在JSON中插入了注释或者多余的逗号这一步还是会失败这时候就老实返回parse_failed不要过度处理因为畸形输出毕竟是少数不值得为它维护一套维修逻辑。5. 常见问题与避坑真实环境里最值得记录的五个踩坑点5.1 标签更新频率过高导致LLM调用成本失控现象系统上线后发现LLM调用量远超预估账单一个月翻了四倍但画像准确率并没有明显提升。原因动态标签更新逻辑设得太激进。用户在一天内产生了大量行为事件每一条都触发了LLM重新推断导致重复计算。比如用户反复浏览了同一件商品系统每次浏览都触发一次全量标签重算。解决在动态标签更新模块增加“变化量阈值”控制只有当新增行为与上一次推断时的行为序列差异超过一定比例时才触发LLM重算。我在类似项目里一般设的是行为序列里新增事件少于5条且无新文本内容时只更新统计特征不触发LLM推断。5.2 文本清洗把关键语气词全洗掉了现象情感识别模块的准确率始终上不去负面评论经常被识别为中性。原因预处理环节用了通用的中文停用词表把“太”“很”“简直”“一点也”这些程度副词全过滤掉了。用户写“太差了”清洗后变成“差”强度分直接掉一半用户写“一点也不满意”清洗后变成“满意”情感方向直接反转。解决在文本预处理管线上专门维护了一份“业务保留词表”程度副词、否定词、语气助词全部保留。同时把通用停用词过滤的优先级调到最低——先做业务词保护再做通用过滤。5.3 时间特征没对齐消费时段标签全是乱的现象消费时段偏好标签在白天黑夜用户之间分布几乎一样标签等于没做。原因日志系统记录的时间是服务器时区的UTC时间用户画像系统按北京时间处理没有做时区转换。晚上8点的活跃用户统计出来变成凌晨4点活跃。解决在所有涉及时间的代码处理链路里统一使用时间戳加时区偏移量的复合格式入库前统一转成东八区。建议在行为表里直接固化一个local_hour字段在数据接入时一次性算好后续所有特征计算都直接读取这个字段不要再做二次换算。5.4 LLM输出中的标签名称不稳定现象同一种消费倾向模型有时候输出“性价比优先”有时候输出“价格敏感”后续做标签聚合时这两条被当成不同标签处理。原因标签体系没有做“别名收敛”。模型在不同上下文中会采用不同的同义表达但系统直接拿模型输出当标签ID使用没有任何映射过程。解决建立“标准标签-别名映射表”在LLM推断完成后统一过一个标准化映射逻辑。如果映射表中发现新别名先进入人工审核表不直接生成新标签。宁可标签粒度粗一点也不要一堆同义标签把画像系统搞乱。5.5 动态标签在下游数据仓库里出现分区漂移现象数仓里用户标签表的数据量没有变但查询结果每天不一样标签字段经常在分区之间“跳来跳去”。原因动态标签是每天计算的但计算时使用了全量历史文本模型每次更新后老文本在不同批次中被赋予的标签不完全一致导致同一用户的历史标签被重写后和新分区数据冲突。解决采用“标签快照增量”模式。每日标签计算完成后写两个表——一个是用户当日最新画像表覆盖写一个是标签变更日志表追加写。下游应用读取最新画像表模型训练读取变更日志表。不追历史、不跨分区比对数据一致性立刻好转。6. 动态标签生成与增量更新从离线画像走向准实时服务的落地技巧动态标签系统上线后真正的分水岭在于“增量更新”的实现质量。全量计算每个人都会做一天跑一次批处理把用户所有历史行为重新喂给模型输出结果覆盖写。但增量更新做不好画像时效性就是一纸空谈。6.1 增量更新的核心机制版本号加变更日志我建议的做法是给每个用户标签增加一个version字段每次增量计算完成后version加1并同时写入一条变更日志记录该用户哪些标签发生了变化、变化前后的值是什么、触发变化的行为事件是哪一条。这样既支持下游的增量消费又保留完整的审计链路。-- 用户最新画像表按 user_id 覆盖写 CREATE TABLE user_profile_latest ( user_id STRING COMMENT 用户ID, profile_snapshot STRING COMMENT 画像JSON快照, label_versions MAPSTRING, INT COMMENT 标签级版本号, update_time TIMESTAMP COMMENT 更新时间 ) PARTITIONED BY (dt STRING); -- 标签变更日志表追加写不做更新 CREATE TABLE user_label_change_log ( user_id STRING COMMENT 用户ID, label_name STRING COMMENT 变更标签名, old_value STRING COMMENT 变更前值, new_value STRING COMMENT 变更后值, trigger_event STRING COMMENT 触发变更的事件, version INT COMMENT 变更后版本号 ) PARTITIONED BY (dt STRING);这套表结构是两个字段起了决定性作用label_versions存储每个标签的独立版本号允许单个标签独立回滚trigger_event记录触发变更的具体行为事件业务人员能直接看到“因为该用户昨天咨询了售后所以新增了‘售后敏感’标签”。没有这两列的画像表出了问题压根没法排查。6.2 增量计算触发条件怎么设计增量计算不能设计成“有事件就触发”要设计成“有信息增量才触发”。我一般会把触发条件分成三档放在配置中心里方便运行中调整A档触发用户产生了新的文本内容比如评论、投诉、客服对话这类事件信息量最大立即触发LLM标签重算。B档触发用户产生了新的关键行为事件比如完成首单、连续三天访问、清空购物车这类事件触发部分标签重算。C档触发用户只是产生普通浏览行为只做统计特征更新不触发LLM推断等数据积累到阈值后再降级到B档处理。这里有一个血泪教训如果你在项目初期把所有行为都当成A档处理LLM调用量很快会爆而且大部分调用都在重复算同一个结果。信息增量判断做得越严格系统运行越稳定。6.3 准实时更新管道从事件入湖到标签刷新的分钟级链路严格意义上这个项目不算完全的实时系统但它的增量更新链路已经能做到分钟级刷新。我把这条管道的核心环节拆一下事件通过埋点SDK进入Kafka后由Flink做轻量级预处理——把原始事件转成标准行为结构、进行用户归属确认、补全会话上下文。预处理完成后判断该事件属于A档、B档还是C档。A档事件直接推给LLM服务做标签重算B档事件进入等待窗口攒5条同类事件后做一次批量重算C档事件只写入行为特征表。这套机制的巧妙之处在于它不追求每一条事件都实时更标签而是用“关键事件驱动、普通事件积累”的方式平衡时效和成本。在实际项目中A档事件占全部事件的5%左右却贡献了80%以上的标签变化。把资源集中在真正有信息量的事件上比完美主义式的全量实时计算要经济得多。从那以后我每次搭类似系统都会强制走一遍这个流程先定触发分档再写版本管理和变更日志最后才动手写特征计算代码。顺序反了后面就是无穷无尽的返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表