第02章 客户画像系统:用Hermes建立"了解你的客户"数据库

第02章 客户画像系统:用Hermes建立"了解你的客户"数据库

“你不能管理你不了解的事情。了解客户,是一切服务的起点。” —— 客户关系管理原则


“了解你的客户”(Know Your Customer,KYC)在金融行业是法规要求,但优秀的理财经理早在 KYC 成为合规条文之前,就已经把深入了解客户当作服务的核心。问题在于:如何在服务 200 位客户时,保持和服务 20 位时同样深入的"了解"?

Hermes 的客户画像系统就是答案。


2.1 客户画像的四个维度

# ====================================================
# 客户画像数据模型设计
# ====================================================

from dataclasses import dataclass, field
from typing import List, Optional, Dict
from enum import Enum
from datetime import date
import json


class RiskLevel(str, Enum):
    """客户风险承受等级(中国监管要求的五级分类)"""
    CONSERVATIVE = "保守型"        # R1 - 保本产品
    CAUTIOUS = "谨慎型"            # R2 - 固收+
    BALANCED = "稳健型"            # R3 - 均衡配置
    AGGRESSIVE = "积极型"          # R4 - 权益为主
    SPECULATIVE = "激进型"         # R5 - 高风险产品


class AssetClass(str, Enum):
    """资产类型"""
    STOCK = "股票"
    FUND = "基金"
    BOND = "债券"
    INSURANCE = "保险"
    TRUST = "信托"
    FOREIGN_EXCHANGE = "外汇"
    REAL_ESTATE = "房产"
    CASH = "现金及存款"


@dataclass
class AssetHolding:
    """单项资产持仓"""
    asset_class: AssetClass
    name: str                          # 产品/股票名称
    code: str                          # 代码
    amount: float                      # 持仓金额(万元)
    cost_price: float                  # 成本价
    current_price: float               # 当前价
    purchase_date: date
    
    @property
    def return_rate(self) -> float:
        """当前收益率"""
        return (self.current_price - self.cost_price) / self.cost_price
    
    @property
    def current_value(self) -> float:
        """当前市值(万元)"""
        return self.amount * (1 + self.return_rate)


@dataclass
class LifeEvent:
    """重要生命节点(主动服务的触发器)"""
    event_type: str    # "生日/结婚纪念/子女升学/退休计划/年终奖"
    date: date
    notes: str = ""
    

@dataclass
class ClientPreference:
    """客户偏好(决定沟通方式和内容)"""
    preferred_channel: str             # "微信/电话/邮件"
    best_contact_time: str             # "工作日上午/周末下午/..."
    report_format: str                 # "详细PDF/简洁摘要/图表为主"
    communication_frequency: str       # "每周/每两周/每月"
    interested_topics: List[str]       # 感兴趣的市场话题
    disliked_topics: List[str]         # 不喜欢讨论的话题
    language_style: str                # "专业术语/通俗易懂"


@dataclass
class ClientProfile:
    """
    完整客户画像
    这是 Hermes Agent 所有决策的核心数据结构
    """
    # 基本信息
    client_id: str
    name: str
    age: int
    occupation: str
    city: str
    
    # 财务状况
    total_aum: float                               # 总资产管理规模(万元)
    annual_income: Optional[float]                 # 年收入(万元)
    holdings: List[AssetHolding] = field(default_factory=list)
    
    # 风险画像
    risk_level: RiskLevel = RiskLevel.BALANCED
    risk_score: int = 60                           # 0-100,量化风险承受度
    investment_horizon: str = "3-5年"             # 投资期限
    
    # 偏好
    preferences: Optional[ClientPreference] = None
    
    # 生命节点
    life_events: List[LifeEvent] = field(default_factory=list)
    
    # 关系维护
    relationship_manager: str = ""                 # 负责的理财经理
    onboarding_date: Optional[date] = None
    last_contact_date: Optional[date] = None
    notes: str = ""                               # 理财经理手动备注
    
    @property
    def portfolio_summary(self) -> Dict:
        """资产组合摘要"""
        by_class = {}
        for holding in self.holdings:
            cls = holding.asset_class.value
            if cls not in by_class:
                by_class[cls] = 0.0
            by_class[cls] += holding.current_value
        
        total = sum(by_class.values()) or 1
        return {
            cls: {"金额(万)": round(amount, 2), "占比": f"{amount/total:.1%}"}
            for cls, amount in sorted(by_class.items(), key=lambda x: -x[1])
        }
    
    @property
    def days_since_contact(self) -> Optional[int]:
        """距上次联系的天数"""
        if self.last_contact_date:
            return (date.today() - self.last_contact_date).days
        return None


# 创建一个示例客户
example_client = ClientProfile(
    client_id="C001",
    name="张先生",
    age=48,
    occupation="民营企业主",
    city="上海",
    total_aum=850.0,
    annual_income=200.0,
    risk_level=RiskLevel.BALANCED,
    risk_score=65,
    investment_horizon="5-10年",
    holdings=[
        AssetHolding(
            asset_class=AssetClass.FUND,
            name="易方达蓝筹精选混合",
            code="005827",
            amount=200.0,
            cost_price=2.50,
            current_price=2.35,
            purchase_date=date(2023, 6, 15),
        ),
        AssetHolding(
            asset_class=AssetClass.TRUST,
            name="XX信托计划",
            code="T2023001",
            amount=300.0,
            cost_price=1.0,
            current_price=1.058,
            purchase_date=date(2023, 1, 10),
        ),
        AssetHolding(
            asset_class=AssetClass.CASH,
            name="活期存款+货基",
            code="CASH",
            amount=350.0,
            cost_price=1.0,
            current_price=1.015,
            purchase_date=date(2024, 1, 1),
        ),
    ],
    preferences=ClientPreference(
        preferred_channel="微信",
        best_contact_time="工作日上午10-11点",
        report_format="简洁摘要+图表",
        communication_frequency="每两周",
        interested_topics=["A股市场", "美联储政策", "房产市场"],
        disliked_topics=["加密货币"],
        language_style="通俗易懂",
    ),
    life_events=[
        LifeEvent("生日", date(1976, 9, 15)),
        LifeEvent("子女高考", date(2026, 6, 7), "孩子准备出国留学"),
    ],
    relationship_manager="李小姐",
    last_contact_date=date(2025, 6, 1),
    notes="企业主,现金流充裕但不喜欢高波动产品。今年计划子女留学,需要美元资产配置建议。",
)

print("=== 客户画像示例 ===")
print(f"客户:{example_client.name},{example_client.age}岁,{example_client.occupation}")
print(f"总资产:{example_client.total_aum} 万元")
print(f"风险等级:{example_client.risk_level.value}(评分:{example_client.risk_score}/100)")
print(f"\n资产组合:")
for asset_class, info in example_client.portfolio_summary.items():
    print(f"  {asset_class}: {info['金额(万)']}万 ({info['占比']})")
print(f"\n距上次联系:{example_client.days_since_contact} 天")

2.2 数据整合:从多个来源构建统一画像

# ====================================================
# 客户数据整合服务
# ====================================================

"""
数据来源整合(实际项目中的常见情况):
    
    来源1:核心系统(CRM/交易系统)
        - 基本信息、KYC 数据
        - 账户持仓、交易记录
        - 合同、协议文件

    来源2:外部市场数据
        - 持仓产品的实时/历史行情
        - 基金净值更新

    来源3:沟通记录系统
        - 历史通话录音(已转写)
        - 微信/邮件沟通记录
        - 理财经理的会议备忘

    来源4:手动更新(理财经理输入)
        - 客户透露的信息(家庭变化、未来计划)
        - 会面印象
        - 特殊需求记录
"""

import asyncio
from datetime import datetime, date
from typing import Optional


class ClientProfileService:
    """
    客户画像服务
    负责整合多源数据,维护客户画像的最新状态
    """
    
    def __init__(self, db_session, market_data_client):
        self.db = db_session
        self.market = market_data_client
    
    async def build_profile(self, client_id: str) -> ClientProfile:
        """
        从多个数据源构建完整客户画像
        (实际项目中各 fetch 替换为真实 API 调用)
        """
        # 并行获取基础数据(无依赖关系的查询并行执行)
        basic_info, holdings_raw, preferences = await asyncio.gather(
            self._fetch_basic_info(client_id),
            self._fetch_holdings(client_id),
            self._fetch_preferences(client_id),
        )
        
        # 更新持仓的当前价格(串行,依赖 holdings_raw)
        updated_holdings = await self._update_holding_prices(holdings_raw)
        
        # 获取生命节点(可并行)
        life_events = await self._fetch_life_events(client_id)
        
        return ClientProfile(
            client_id=client_id,
            name=basic_info["name"],
            age=basic_info["age"],
            occupation=basic_info["occupation"],
            city=basic_info["city"],
            total_aum=sum(h.current_value for h in updated_holdings),
            risk_level=RiskLevel(basic_info["risk_level"]),
            risk_score=basic_info["risk_score"],
            holdings=updated_holdings,
            preferences=preferences,
            life_events=life_events,
            relationship_manager=basic_info["rm_name"],
            last_contact_date=basic_info.get("last_contact_date"),
            notes=basic_info.get("notes", ""),
        )
    
    async def update_contact_record(
        self,
        client_id: str,
        channel: str,
        summary: str,
        rm_notes: str = "",
    ):
        """
        记录每次客户沟通(让画像保持最新)
        """
        record = {
            "client_id": client_id,
            "timestamp": datetime.now().isoformat(),
            "channel": channel,
            "summary": summary,
            "rm_notes": rm_notes,
        }
        # 存储到数据库
        await self.db.execute(
            "INSERT INTO contact_records VALUES (:client_id, :timestamp, :channel, :summary, :rm_notes)",
            record,
        )
        # 更新 last_contact_date
        await self.db.execute(
            "UPDATE clients SET last_contact_date = :today WHERE client_id = :client_id",
            {"today": date.today(), "client_id": client_id},
        )
    
    async def add_life_event(
        self,
        client_id: str,
        event_type: str,
        event_date: date,
        notes: str = "",
    ):
        """
        添加生命节点(由理财经理手动输入,或从沟通记录中提取)
        """
        await self.db.execute(
            """INSERT INTO life_events (client_id, event_type, event_date, notes)
               VALUES (:client_id, :event_type, :event_date, :notes)""",
            {
                "client_id": client_id,
                "event_type": event_type,
                "event_date": event_date.isoformat(),
                "notes": notes,
            },
        )
    
    # -------- 私有方法(与实际系统集成时替换) --------
    
    async def _fetch_basic_info(self, client_id: str) -> dict:
        """从 CRM 系统获取基本信息"""
        # 实际实现:await self.db.fetchone("SELECT ... FROM clients WHERE ...")
        return {
            "name": "张先生",
            "age": 48,
            "occupation": "民营企业主",
            "city": "上海",
            "risk_level": "稳健型",
            "risk_score": 65,
            "rm_name": "李小姐",
            "last_contact_date": date(2025, 6, 1),
            "notes": "今年计划子女留学",
        }
    
    async def _fetch_holdings(self, client_id: str) -> list:
        """从交易系统获取持仓"""
        return []  # 实际实现中返回 AssetHolding 列表
    
    async def _update_holding_prices(self, holdings: list) -> list:
        """批量更新持仓当前价格"""
        # 实际实现:调用市场数据 API 批量更新
        return holdings
    
    async def _fetch_preferences(self, client_id: str) -> ClientPreference:
        """获取客户偏好设置"""
        return ClientPreference(
            preferred_channel="微信",
            best_contact_time="工作日上午10-11点",
            report_format="简洁摘要+图表",
            communication_frequency="每两周",
            interested_topics=["A股市场", "美联储政策"],
            disliked_topics=["加密货币"],
            language_style="通俗易懂",
        )
    
    async def _fetch_life_events(self, client_id: str) -> list:
        """获取客户生命节点"""
        return [
            LifeEvent("生日", date(1976, 9, 15)),
            LifeEvent("子女高考", date(2026, 6, 7)),
        ]


print("客户画像服务初始化完成")
print("支持:基本信息·持仓·偏好·生命节点 四维数据整合")

2.3 Hermes 如何使用客户画像

# ====================================================
# Hermes Agent 使用客户画像进行个性化决策
# ====================================================

import asyncio
import json
from openai import AsyncOpenAI

client = AsyncOpenAI()


def build_client_context(profile: ClientProfile) -> str:
    """
    将客户画像转换为 Hermes 可以使用的上下文字符串
    这是 System Prompt 的一部分
    """
    upcoming_events = [
        f"{e.event_type}({e.date})"
        for e in profile.life_events
        if (e.date - date.today()).days <= 30  # 30天内的事件
    ]
    
    context = f"""
## 当前服务客户信息

**基本信息**
- 姓名:{profile.name},{profile.age}岁,{profile.occupation}
- 城市:{profile.city}
- 风险等级:{profile.risk_level.value}(评分 {profile.risk_score}/100)
- 投资期限:{profile.investment_horizon}

**资产状况**
- 总资产规模:{profile.total_aum:.0f} 万元
- 主要持仓:{list(profile.portfolio_summary.keys())[:3]}

**沟通偏好**
- 首选渠道:{profile.preferences.preferred_channel if profile.preferences else '未设置'}
- 最佳联系时间:{profile.preferences.best_contact_time if profile.preferences else '未设置'}
- 报告风格:{profile.preferences.report_format if profile.preferences else '未设置'}
- 感兴趣话题:{', '.join(profile.preferences.interested_topics) if profile.preferences else '未设置'}

**重要备注**
{profile.notes}

**近期生命节点**
{', '.join(upcoming_events) if upcoming_events else '未来30天无特殊节点'}

**跟进状态**
- 距上次联系:{profile.days_since_contact} 天
- 建议联系频率:{profile.preferences.communication_frequency if profile.preferences else '每两周'}
"""
    return context.strip()


async def hermes_personalized_response(
    client_profile: ClientProfile,
    user_query: str,
    market_context: str = "",
) -> str:
    """
    基于客户画像生成个性化回复
    展示 Hermes 如何用画像定制每位客户的回复
    """
    client_ctx = build_client_context(client_profile)
    
    system_prompt = f"""你是一位专业的理财顾问 AI 助理,正在为特定客户服务。

{client_ctx}

服务原则:
1. 根据客户的风险等级和偏好定制建议,不推荐超出其风险承受能力的产品
2. 使用客户偏好的语言风格({client_profile.preferences.language_style if client_profile.preferences else '通俗易懂'})
3. 所有投资建议标注"仅供参考,最终决策请与您的理财顾问确认"
4. 不透露其他客户的信息
5. 复杂问题引导客户联系理财经理李小姐进行深入沟通"""
    
    response = await client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_query},
        ],
        max_tokens=600,
    )
    
    return response.choices[0].message.content


# 演示
async def demo():
    query = "最近市场跌了不少,我的基金是不是该赎回?"
    print(f"客户询问:{query}\n")
    
    # 构建客户上下文
    context = build_client_context(example_client)
    print("=== 客户上下文(Hermes 看到的) ===")
    print(context[:400], "...\n")
    
    print("基于画像的个性化回复将根据张先生的:")
    print("  - 风险等级(稳健型):不建议激进操作")
    print("  - 子女留学计划:考虑流动性需求")
    print("  - 语言偏好(通俗易懂):避免过多专业术语")

asyncio.run(demo())

本章小结

  1. 客户画像是 Hermes 做出好决策的基础:没有完整的客户画像,AI 只能给出通用的模板化建议;有了画像,才能真正做到"了解你的客户",给出与其风险偏好、资产情况、生活阶段相匹配的个性化服务。

  2. 四维画像结构:基本信息与资产状况、风险偏好、沟通偏好、生命节点——这四个维度共同构成一个完整的客户视角。其中最容易被忽视的是"生命节点",但恰恰是主动服务的最大机会来源。

  3. 数据整合必须是渐进式的:不要试图一次性完美地收集所有数据。从基本信息+持仓开始,在每次互动中逐渐补全偏好和生命节点信息。Hermes 能从沟通记录中自动提取新信息,持续丰富画像。

  4. 沟通偏好决定服务体验:同样的投资信息,用不同的渠道、不同的语言风格、在不同的时间发送,效果可以天壤之别。记录并遵守每位客户的偏好,是从"服务合格"到"服务优秀"的关键。

  5. 画像维护需要人机协作:自动化可以更新持仓行情、追踪市场信号,但很多重要的客户信息(最近换工作、刚离婚、打算移民)只有理财经理才知道。设计好"人工补充备注"的界面和流程,让画像真正保持最新。

# 核心行动:为你最重要的 10 位客户创建数字画像
# 从以下字段开始(15分钟内可以完成一位客户)

QUICK_PROFILE_TEMPLATE = {
    "姓名": "",
    "年龄": 0,
    "职业": "",
    "总资产(万)": 0,
    "风险等级": "稳健型",
    "主要持仓": [],             # 最大的3项资产
    "首选联系方式": "",          # 微信/电话/邮件
    "最佳联系时间": "",
    "最近一次重要对话要点": "",  # 记录客户说过的重要信息
    "未来3个月的重要节点": [],  # 生日/年终奖/子女考试等
}

本章提示词模板

【模板1:客户画像提取提示词】
以下是我和客户的一段沟通记录(已脱敏):

{conversation_text}

请帮我提取以下信息,以JSON格式输出:
{
  "风险偏好线索": [],      // 客户说的话中透露的风险态度
  "生活阶段信息": [],      // 家庭、工作、子女等信息
  "投资关注点": [],        // 客户关心的市场或产品
  "顾虑和担忧": [],        // 客户提到的不安或担忧
  "行动意向": [],          // 客户有意向做的事
  "下次跟进建议": ""       // 建议的下次沟通重点
}
【模板2:画像完整性检查提示词】
我有一位客户的以下画像信息:

{client_profile_summary}

请评估这份画像的完整性:
1. 哪些关键信息是缺失的?(按重要性排序)
2. 基于现有信息,有哪些主动服务机会?
3. 下次与客户沟通时,建议收集哪些信息来补充画像?
4. 当前画像能支持哪些类型的个性化服务?哪些还不能支持?

→ 第03章:风险偏好分析:AI如何读懂客户的投资心理