☰
LLM智能用户画像:非结构化文本驱动的动态标签生成与增量更新实践
2026/10/4 1:11:52 网站建设 项目流程

简介:一套基于大语言模型的智能用户画像分析系统完整参考实现,面向企业营销、用户运营与数据分析从业者,聚焦利用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_len=20): """ 将用户行为事件序列压缩为带权重的结构化字典 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(), key=lambda 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, key=level_scores.get) confidence = level_scores[best_level] return { "level": best_level, "confidence": round(float(confidence), 3), "feature_note": f"avg_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 输出 "unknown",confidence 为 0 """ client = model_client response = client.chat.completions.create( model="llm_provider_model", messages=[{"role": "user", "content": prompt}], temperature=0.2, response_format={"type": "json_object"}, ) import json result = json.loads(response.choices[0].message.content) return result

代码里temperature=0.2是刻意调低的值,画像是判断型任务,不是创意型任务,温度太高会让模型文体不稳定,同一批文本两次跑出的价值观标签可能不一致。response_format=json_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:end+1] 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:end+1]这段是从第一个左花括号取到最后一个右花括号,中间的文本理论上必须是一段合法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 MAP<STRING, 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%以上的标签变化。把资源集中在真正有信息量的事件上,比完美主义式的全量实时计算要经济得多。

从那以后,我每次搭类似系统都会强制走一遍这个流程:先定触发分档,再写版本管理和变更日志,最后才动手写特征计算代码。顺序反了,后面就是无穷无尽的返工。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询