跳到主内容
快讯直播
AI智模界
教程

用大模型做用户画像分析:从原始评论到画像卡

这篇能做出什么

假设手里有一份用户反馈表:电商评论、客服会话记录、问卷开放题、App 内的意见反馈,几百到几万条不等。这些文本里其实藏着"用户是谁、想要什么、卡在哪一步",但一条条读太慢,靠关键词搜索又抓不住语义。

做完这套流程,你会得到四样东西:

1. 一份结构化标签表:每条反馈都带上"目标 / 场景 / 痛点 / 现有替代方案 / 决策因素 / 付费意愿档位"等字段,导出成 CSV,能直接丢进 BI 或 Excel 做透视。

2. 标签分布统计:比如"售后场景下 62% 的痛点是客服响应",交叉表一拉就出来。

3. 3~6 张用户画像卡:每张包含画像名、典型场景、核心诉求、主要顾虑、可执行建议、代表原话。可以直接放进需求评审或投放 brief。

4. 一套可复现的脚本目录:换一批数据重跑即可,支持断点续跑,不用每次从头烧 token。

举个具体例子。原始输入是一条评论:

> 买了两次都因为发货太慢退了,客服回复也慢,后来干脆去楼下超市买了,虽然贵点但当天能拿到。

跑完流程后,这条记录会变成:scenario=售后pain=速度workaround=线下超市decision=客服响应quote=后来干脆去楼下超市买了。几百条这样的记录聚合起来,"急用型用户"这张画像卡就自己浮出来了。

前置条件清单

  • Python 环境,能 pip install 即可,建议用虚拟环境隔离依赖。
  • 一个模型接口:OpenAI 兼容风格的 base_url + api_key + 模型名,具体模型和参数以所用服务的官方文档当前版本为准。文本本身不强求大模型,中等规模的模型抽取标签通常够用。
  • 可选:embedding 接口。如果想把反馈自动聚成簇,需要 embedding;也可以用本地 embedding 模型,避免把原始文本发到外部。
  • 数据:至少 200 条文本起步,少于这个量做分布统计意义不大。字段里最好有一个稳定的唯一 ID。
  • 磁盘上的断点文件:跑批过程中随时可能中断,落盘是必须的。

推荐的目录结构:

```text

persona/

data/raw.csv # 原始数据,只读

data/clean.csv # 清洗脱敏后

prompts/extract.md # 抽取提示词

prompts/persona.md # 画像卡提示词

out/labels.jsonl # 逐条结果,断点文件

out/labels.csv # 展开后的表格

out/report.md # 最终报告

src/01_clean.py

src/02_extract.py

src/03_analyze.py

src/04_persona.py

```

依赖大致是这些:

```bash

pip install pandas scikit-learn openai

```

步骤 1:先定字段,再动手

这一步不做,后面全是返工。先回答一个问题:这份画像要拿去做决策吗?如果只是"看看用户长什么样",字段可以随便定;如果要据此改产品、改话术、改投放,字段必须能落到行动上。

一个好用的字段集:

字段含义取值
goal想达成什么自由文本,一句话
scenario使用场景枚举
pain主要痛点枚举
workaround当前替代方案自由文本,无则 none
decision决策因素枚举
pay_level付费意愿档位低 / 中 / 高 / 未知
quote代表原话逐字摘录

三条经验:枚举值控制在 5~8 个,太多模型会飘;每个枚举都要有"其他"兜底;不要设计"用户性格""用户情绪"这类无法验证、也无法行动的字段。

步骤 2:清洗与脱敏

先把隐私处理掉,再谈分析。手机号、邮箱、证件号、订单号这类内容用正则替换成占位符,既合规,也不影响语义。

```python

import re

import pandas as pd

df = pd.read_csv("data/raw.csv")

df = df.dropna(subset=["text"]).drop_duplicates(subset=["text"])

PATTERNS = [

(re.compile(r"1[3-9]\d{9}"), "[手机号]"),

(re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+"), "[邮箱]"),

(re.compile(r"\b\d{17}[\dXx]\b"), "[证件号]"),

(re.compile(r"\b\d{12,}\b"), "[单号]"),

]

def scrub(s: str) -> str:

for p, r in PATTERNS:

s = p.sub(r, s)

return s

df["text"] = df["text"].astype(str).str.strip().map(scrub)

df = df[df["text"].str.len().between(5, 2000)]

df.to_csv("data/clean.csv", index=False)

print(len(df))

```

长度上限的作用是防止个别超长文本把单次请求撑爆;下限是滤掉"好""谢谢"这类没有信息量的内容。

步骤 3:把标签体系写进提示词

把提示词单独存成文件,方便版本管理和对比效果。关键是三点:明确只输出 JSON、明确枚举范围、要求摘录必须逐字来自原文。

prompts/extract.md

```markdown

你是用户研究分析师。请从下面这条用户反馈中抽取结构化标签。

只输出一个 JSON 对象,不要输出解释,不要加代码块围栏。

字段定义:

  • goal: 用户想达成什么。字符串,一句话。
  • scenario: 使用场景。从 [工作, 学习, 生活, 购物, 售后] 中选一个。
  • pain: 主要痛点。从 [价格, 效果, 速度, 易用性, 稳定性, 客服, 其他] 中选一个。
  • workaround: 用户当前的替代方案。没有就写 "none"。
  • decision: 影响决策的因素。从 [口碑, 价格, 试用体验, 客服响应, 品牌, 其他] 中选一个。
  • pay_level: 付费意愿。从 [低, 中, 高, 未知] 中选一个。
  • quote: 最能代表上述判断的原文片段,必须逐字来自原文,不超过 40 字。

用户反馈:

"""

{{TEXT}}

"""

```

要求"逐字来自原文"很关键。模型很容易顺手润色或概括,一旦加工过,后面就没法拿原话给业务方看。

步骤 4:单条打通,再做批量

先拿一条数据跑通,确认返回能被解析,再考虑并发。

```python

import json

import os

import time

from openai import OpenAI

client = OpenAI(

api_key=os.environ["LLM_API_KEY"],

base_url=os.environ["LLM_BASE_URL"],

)

MODEL = os.environ["LLM_MODEL"] # 模型名以所用服务官方文档当前版本为准

PROMPT = open("prompts/extract.md", encoding="utf-8").read()

ALLOWED = {

"scenario": {"工作", "学习", "生活", "购物", "售后"},

"pain": {"价格", "效果", "速度", "易用性", "稳定性", "客服", "其他"},

"decision": {"口碑", "价格", "试用体验", "客服响应", "品牌", "其他"},

"pay_level": {"低", "中", "高", "未知"},

}

def strip_fence(s: str) -> str:

s = s.strip()

if s.startswith("```"):

s = s.split("\n", 1)[1] if "\n" in s else s

s = s.rsplit("```", 1)[0]

return s.strip()

def extract(text: str, retries: int = 4) -> dict:

prompt = PROMPT.replace("{{TEXT}}", text)

for i in range(retries):

try:

resp = client.chat.completions.create(

model=MODEL,

messages=[{"role": "user", "content": prompt}],

temperature=0,

)

data = json.loads(strip_fence(resp.choices[0].message.content))

for key, allowed in ALLOWED.items():

if data.get(key) not in allowed:

data[key] = "其他" if "其他" in allowed else "未知"

if data.get("quote") and data["quote"] not in text:

data["quote"] = ""

return data

except Exception as e:

if i == retries - 1:

return {"error": str(e)}

time.sleep(2 ** i)

return {}

```

两个细节:temperature=0 能让同一批数据重跑的结果更稳定,但并非所有服务都支持这个参数,遇到报错就去掉;quote 校验用子串判断,不匹配就清空,这是防幻觉的兜底。

步骤 5:批量抽取与断点续跑

落盘用 JSONL,一行一条,天然支持追加和续跑。启动时先读一遍已有结果的 ID,跳过处理过的。

```python

import json

import os

from concurrent.futures import ThreadPoolExecutor

import pandas as pd

df = pd.read_csv("data/clean.csv")

out_path = "out/labels.jsonl"

done = set()

if os.path.exists(out_path):

with open(out_path, encoding="utf-8") as f:

for line in f:

try:

done.add(json.loads(line)["id"])

except Exception:

continue

todo = df[~df["id"].isin(done)]

def work(row):

r = extract(str(row["text"]))

r["id"] = row["id"]

return r

with open(out_path, "a", encoding="utf-8") as f, \

ThreadPoolExecutor(max_workers=4) as pool:

for r in pool.map(work, [row for _, row in todo.iterrows()]):

f.write(json.dumps(r, ensure_ascii=False) + "\n")

f.flush()

```

并发数从 4 起,遇到限流就降;如果服务端有限流提示,按它的建议调整间隔。先拿 50 条试跑,估算一下总耗时和用量,再决定要不要全量。

步骤 6:统计标签分布

```python

import json

import pandas as pd

rows = [json.loads(l) for l in open("out/labels.jsonl", encoding="utf-8")]

rows = [r for r in rows if "error" not in r]

lab = pd.DataFrame(rows)

print(lab["pain"].value_counts())

print(pd.crosstab(lab["scenario"], lab["pain"], normalize="index").round(2))

lab.to_csv("out/labels.csv", index=False)

```

交叉表是最容易出结论的地方。"售后 × 客服"占比高,说明问题在服务流程;"购物 × 价格"占比高,说明该调整定价或促销策略。这一步不需要模型,纯统计。

步骤 7:聚成簇,再写成画像卡

有了标签,可以直接按 pain 分组建卡,也可以先做聚类,让数据自己分出人群。聚类用 embedding 加 KMeans 比较省事:

```python

import numpy as np

from sklearn.cluster import KMeans

from openai import OpenAI

client = OpenAI(

api_key=os.environ["LLM_API_KEY"],

base_url=os.environ["LLM_BASE_URL"],

)

EMB_MODEL = os.environ["EMBEDDING_MODEL"] # 以所用服务官方文档当前版本为准

texts = lab["text"].fillna("").tolist()

vecs = []

for i in range(0, len(texts), 64):

batch = texts[i:i + 64]

resp = client.embeddings.create(model=EMB_MODEL, input=batch)

vecs.extend([d.embedding for d in resp.data])

X = np.array(vecs)

X = X / np.linalg.norm(X, axis=1, keepdims=True) # 归一化后更稳

k = 5

lab["cluster"] = KMeans(n_clusters=k, n_init=10, random_state=0).fit_predict(X)

```

k 取多少没有标准答案。先用 4~6 试,看看每簇的样本量是否均衡、簇内文本是否真的像一类人。簇太小(比如只有三五条)就并回去。也可以跳过聚类,直接按 scenario + pain 组合分组,解释性更强,适合给非技术同事讲。

分组定下来后,把每簇的标签分布和一批原话交给模型写画像卡。prompts/persona.md

```markdown

下面是同一簇用户的标签分布和反馈摘录。请写一张用户画像卡。

包含:

1. 画像名(6 字以内,要能和其他簇区分)

2. 一句话特征

3. 典型场景

4. 核心诉求(不超过 3 条)

5. 主要顾虑(不超过 3 条)

6. 可执行建议(产品、运营、客服各 1 条)

7. 代表原话(3 条,逐字引用,不要改写)

不要编造没出现过的信息,信息不足的地方写"数据不足"。

标签分布:

{{DIST}}

反馈摘录:

{{SAMPLES}}

```

最后一定要人工过一遍。挑出明显跑偏的画像卡,回头看是抽取阶段标签错了,还是聚类把两类人混在一起了,再针对性调整。

常见坑与排错

模型返回带了代码块围栏json.loads 直接报错。用 strip_fence 先剥一层,或者干脆在提示词里强调"不要加围栏",两者一起上更稳。

同一意思出现多个标签。比如"贵""价格高""性价比低"被抽成三个不同的值。解决办法是枚举固定 + 后处理归一,把不在枚举里的值统一映射到"其他",再人工看一遍"其他"里都是什么,决定要不要扩充枚举。

模型编造原话quote 字段看着像原文其实被改写了。脚本里做子串校验,不匹配就置空,宁可缺也不要假。这一点在拿原话给业务方看时尤其重要。

并发一开就被限流。把 max_workers 降到 2,重试间隔改长,或者按时间窗口分批发送。断点续跑能保证限流不会让你从头再来。

长文本被截断。超长反馈可以先按句切分,逐段抽取再合并;或者先让模型做一次摘要,再对摘要抽标签。两种都会损失细节,按分析目的取舍。

样本本身有偏差。评论区的用户和沉默用户不是一类人,问卷回答者也不是。做结论时把数据来源写进报告,避免把"爱评论的人"当成"全部用户"。

成本失控。全量跑之前先用 50 条测一遍,按实际 token 用量线性估算。抽取和聚类是两笔开销,embedding 通常比对话便宜,可以先做聚类看效果。

只看分布不看原文。统计表告诉你"客服痛点占 40%",但到底是响应慢、态度差还是权限不够,只有翻原话才知道。每张画像卡至少配三条逐字原话。

下一步建议

跑通一次之后,可以考虑这几件事:

  • 做小规模人工校验。随机抽 50 条,人工标一遍标签,和模型结果比一致率。这个数字决定了后续能不能信任自动统计。
  • 接入增量更新。把脚本挂到定时任务上,每周跑一次新增反馈,画像卡里的趋势变化比单次快照更有价值。
  • 把标签接到 BIout/labels.csv 可以直接当明细表用,配合时间维度看痛点迁移。
  • 用画像卡驱动具体动作。给投放团队改文案、给客服团队改话术、给产品团队排优先级,每张卡对应一个具体交付物,而不是一份只能看的 PPT。
  • 建立标签漂移监控。定期看"其他"这个兜底标签的占比,它持续升高通常意味着业务变了、标签体系该更新了。

标签:#用户画像 #大模型应用 #提示词工程 #数据分析 #Python #用户研究

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。