技术实践

招行 AI 数据科学竞赛参赛:用 LangGraph 构建养老规划 Agent

记录参加招商银行养老规划 Agent 建设比赛的经历,分享基于 LangGraph ReAct Agent 的技术方案设计、Memory 记忆系统实现、Q15 建议书生成器开发,以及参赛过程中的反思与收获。最终得分 128 分。

一、题目介绍

1.1 比赛背景

根据第七次全国人口普查数据,我国 60 岁及以上人口已达 2.64 亿,占总人口的 18.7%,养老需求呈现多元化、个性化趋势。然而,金融行业传统的养老规划服务高度依赖客户经理的个人经验,存在标准化程度低、数据整合难、测算不透明等痛点。

招商银行举办了这次养老规划 Agent 建设比赛,聚焦探究如何将复杂的养老规划 SOP 固化为可自动执行的模块化工作流,实现"数据驱动、标准可验、个性定制"的新一代养老规划服务。

1.2 核心任务

比赛要求参赛者为银行客户经理(RM)设计一个 Agent,帮助 RM 更好地为客户提供养老规划服务。Agent 需要通过访问数据库、做数据分析的方式来对客户开展个性化分析,并最终形成投资建议书。

1.3 评分规则

  • 回答质量:50 分(140 道题,Q15 建议书每题 10 分,其他每题 1 分)
  • 响应时间:10 分(排名制)
  • Token 消耗:10 分(排名制)

其中 Q15 建议书类题目分值占比约 40%,是整场比赛的胜负手。


二、保密要求

【重要声明】 本文内容已做脱敏处理,不涉及任何比赛题目数据、评分细节等敏感信息。以下为参赛过程中需遵守的保密规范,供读者了解比赛的合规要求。

参赛者须严格遵守招商银行保密规定,严守各环节信息,并承担保密义务,不以任何方式(直接、间接、口头或书面形式等)将赛题等内容披露给他人和任何第三方,包括但不限于向外询问、使用辅导等;不擅自在社交平台或媒体平台发布有关的信息和言论。若违反相关要求,组织方将随时取消参赛者的参赛资格。

请勿将竞赛题目外传,平台将对参赛者的竞赛全程进行监测,并对您的提交结果进行审查。一经发现有作弊行为,组织方将取消参赛者的参赛资格,并录入招商银行校园招聘诚信档案。


三、竞赛内容

3.1 题型分类

评测系统依次传入预设问题,Agent 需要识别问题类型并调用相应工具回答。题目大致分为以下几类:

数据查询类:直接查询数据库

  • Q1:客户 V500001 现在年龄多大?
  • Q2:有多少客户年龄在 30 岁及以上?
  • Q3:客户对什么类型的产品行为最多?
  • Q4:浏览权益类产品 2 次以上的客户平均年龄?

金融计算类:调用计算引擎

  • Q5:客户距退休还有多久?(延迟退休政策计算)
  • Q6:客户退休时每月需要支出多少钱?(通胀测算)
  • Q7:退休时最低需要积攒多少钱?(养老金缺口计算)
  • Q8:退休时可以积攒下多少钱?(储蓄终值计算)
  • Q9:全部投资定期存款能否达成目标?(可行性分析)
  • Q14:两阶段通胀下的养老金需求

推荐配置类:资产配置方案

  • Q10:未来一周最可能购买的产品?(行为预测)
  • Q11:预期寿命延长应增加什么产品配置?
  • Q12:最大化投资收益的配置方案
  • Q13:最小化风险波动的配置方案

建议书类:Q15 养老规划建议书(每题 10 分)

3.2 Q15 的特殊要求

Q15 要求生成包含七个章节的养老规划建议书,这是整场比赛的关键难点:

  1. 基本情况 — 客户 ID、年龄、性别、风险评级、净资产、收支结构
  2. 基本假设 — 预期寿命 80 岁、通胀率 2%、退休年龄(延迟退休政策)
  3. 养老目标 — 客户的退休生活目标
  4. 退休后财富需求测算 — 总需求、退休金现值、资金缺口
  5. 产品偏好 — 基于行为数据的偏好分析
  6. 资产配置方式与具体方案 — 配置策略 + 具体比例
  7. 其他建议 — 个性化投资建议

3.3 假设 vs 观点:最关键的规则

Q15 的解析中有一条极其重要的规则:Agent 需要正确区分假设场景真实观点

类型 触发词 处理方式
假设场景 如果、假如、假设 不持久化,不影响后续回答
真实观点 想要、认为、希望 持久化,影响 Q15 建议书内容

具体来说:

  • Q12:"如果客户想要追求投资收益最大化" → 这是假设场景,不应该影响 Q15 的资产配置方案
  • Q13:"客户想要最小化风险波动" → 这是真实观点,应该影响 Q15 的资产配置方案
  • Q6:"客户想要退休后消费水平不下降" → 这是真实观点,应该影响 Q15 的养老目标

这意味着 Agent 必须具备跨题的记忆能力,而且要能正确区分假设和观点。


四、开发框架

4.1 架构选型

采用 LangGraph + ReAct Agent 作为核心框架,原因如下:

  1. ReAct 模式强制工具调用:Agent 必须先调用工具获取数据,再回答问题,避免 LLM 幻觉数值
  2. 工具解耦:每种题型对应独立的工具函数,职责清晰
  3. 状态管理:LangGraph 的 StateGraph 机制天然支持多轮对话

4.2 技术栈

LangGraph (ReAct Agent)
    ├── LangChain (工具定义 + 消息管理)
    ├── ChatOpenAI (兼容 OpenAI API 的 LLM 调用)
    └── Python 3 (标准库)

LLM 使用比赛提供的 API,模型为 qwen3.6-flash,通过 one-api 代理服务调用。

4.3 模块划分

submit/
├── run.py          # 入口文件,评测系统调用 run(inf)
├── agent.py        # ReAct Agent + 12 个工具定义
├── memory.py       # 记忆系统(跨题持久化)
├── tools.py        # Q15 建议书生成器
├── calculator.py   # 金融计算引擎
├── config.py       # 配置文件(常量 + 数据库连接)
├── db.py           # 数据库访问层
└── llm.py          # LLM 调用封装

五、框架细节

5.1 Agent 工具定义

为 Agent 定义了 12 个工具函数,覆盖所有题型:

@tool
def query_customer_info(customer_id: str) -> str:
    """查询客户基本信息"""
    ...

@tool
def calc_retirement_time(customer_id: str) -> str:
    """计算距退休时间"""
    ...

@tool
def calc_inflated_expense(customer_id: str) -> str:
    """计算退休后月支出(考虑通胀)"""
    ...

@tool
def calc_funding_need(customer_id: str) -> str:
    """计算最低需积攒金额"""
    ...

@tool
def calc_savings(customer_id: str, return_rate: float = 0.02) -> str:
    """计算可积攒金额"""
    ...

@tool
def query_product_behavior(customer_id: str) -> str:
    """查询产品行为偏好"""
    ...

@tool
def execute_sql_query(sql: str) -> str:
    """执行 SQL 查询"""
    ...

@tool
def generate_proposal(customer_id: str) -> str:
    """生成养老规划建议书"""
    ...

每个工具函数内部封装了完整的业务逻辑(数据库查询 + 计算逻辑),Agent 只需正确识别问题类型并调用对应工具,不需要自己进行任何数值计算。

5.2 Memory 记忆系统

Memory 系统是整个方案的灵魂,负责跨题持久化客户的观点信息。

核心数据结构

self.memory[customer_id] = {
    "views": [],               # 原始观点记录
    "preferences": [],         # 行为偏好
    "goals": [],               # 养老目标(第3章使用)
    "strategy": None,          # 配置策略(第6章使用)
    "product_preferences": []  # 产品偏好
}

假设 vs 观点区分

def analyze_and_store(self, question: str, answer: str) -> Dict:
    # 假设场景:不持久化
    if "如果" in question or "假如" in question or "假设" in question:
        return {"type": "假设", "should_persist": False}

    # 真实观点:持久化
    if "想要" in question or "认为" in question or "预期" in question:
        customer_id = self._extract_customer_id(question)
        
        # 提取配置策略
        if "最小化风险" in question or "风险波动" in question:
            self.memory[customer_id]["strategy"] = "min_risk"
        elif "最大化收益" in question:
            self.memory[customer_id]["strategy"] = "max_return"

        # 提取养老目标
        if "消费水平不下降" in question:
            self.memory[customer_id]["goals"].append("退休后消费水平不下降")

        return {"type": "观点", "should_persist": True}

持久化机制

记忆通过 memory.jsonhistory.json 文件持久化。每道题回答后,run.py 调用 memory.analyze_and_store() 提取信息,然后 memory.save() 写入文件。Q15 生成建议书时,重新加载记忆文件读取历史观点。

5.3 Q15 建议书生成器

Q15 是最复杂的模块,需要整合客户信息、金融计算、行为统计和记忆系统。

生成流程

1. 提取客户 ID → 查询数据库
2. 获取行为统计 → 查询 action_table
3. 读取记忆摘要 → memory.get_customer_summary()
4. 金融计算 → 通胀测算、养老金缺口、资产配置
5. 组装 7 个章节 → 生成完整建议书

第 3 章养老目标(读取 memory.goals)

customer_summary = memory.get_customer_summary(customer_id)
goals = customer_summary.get("goals", [])
has_consumption_goal = any("消费水平不下降" in g for g in goals)

if has_consumption_goal:
    chapter3 = f"每月可花费与当前 {_fmt(current_expense)} 元购买力相同的金额(退休时约为 {_fmt(inflated_expense)} 元)。"

第 6 章资产配置(读取 memory.strategy)

strategy = customer_summary.get("strategy", "min_risk")

if strategy == "min_risk":
    # 找出能覆盖缺口的最低收益率合规产品
    # 计算最低配置比例:min_ratio = ceil(缺口 / 全仓积累 × 100)
    # 剩余分配:现金理财 + 年金险
    strategy_desc = "客户偏好最小化风险方案"
else:
    # 最高收益合规产品 100%
    strategy_desc = "客户偏好最大化收益方案"

5.4 金融计算引擎

金融计算引擎独立封装在 calculator.py 中,确保数值精度(偏离需在 0.1% 以内)。

核心计算公式

# 退休时月支出(通胀测算)
def calculate_inflated_monthly_expense(current_expense, years_to_retire, inflation_rate=0.02):
    months = int(years_to_retire * 12)
    factor = (1 + inflation_rate / 12) ** months
    return round_int(current_expense * factor)

# 退休金先付年金现值
def calculate_pension_present_value(monthly_pension, months_after_retire, discount_rate=0.02):
    monthly_rate = discount_rate / 12
    v_pow_n = (1 + monthly_rate) ** (-months_after_retire)
    pv_sum = (1 - v_pow_n) * (1 + monthly_rate) / monthly_rate
    return round_int(monthly_pension * pv_sum)

# 延迟退休计算
def calculate_months_to_retirement(current_age, gender, is_cadre=True):
    # 男职工原60岁:每4个月延迟1个月,最终63岁
    # 女干部原55岁:每4个月延迟1个月,最终58岁
    ...

5.5 数据库访问层

比赛要求"必须通过生成 SQL 调用数据库的形式来获取用户信息,严禁将全量数据拉取到本地"。提供了两个层面的数据库访问:

  1. 封装查询get_customer_info(customer_id)get_product_action_stats(customer_id)
  2. 自由 SQLexecute_sql_query(sql) 供 Agent 处理聚合查询(如 Q2、Q4)

产品行为到产品库的映射规则:

产品库 action_table 条件
现金理财 prod_sub_typ='现金' AND prod_typ='理财'
定期存款 prod_sub_typ='一般性' AND prod_typ='存款'
固收+产品 prod_typ IN ('理财','基金') AND rsk_lvl='R3'
权益类产品 prod_typ='基金' AND rsk_lvl IN ('R4','R5')
年金险 prod_sub_typ IN ('税延养老年金','养老年金') AND prod_typ='保险'

六、反思总结

6.1 方案演进历程

主要经历了三个版本的迭代:

V1(submit,LLM 驱动记忆)

  • 使用 LLM 对每道题进行语义分析,提取观点信息
  • 优势:语义理解精准,能处理复杂表达
  • 劣势:Token 消耗高,响应慢

V2(submit1,关键词匹配)

  • 纯关键词匹配,不调用 LLM
  • 优势:轻量快速,Token 消耗低
  • 劣势:缺少 goals/strategy 字段存储,第 3 章固定模板,第 6 章字符串匹配不稳定
  • 得分:123.3 分

V3(最终版,融合优化)

  • 关键词过滤 + 结构化存储
  • 继承 submit1 的轻量特性,添加 submit 的结构化字段
  • 得分:128 分

6.2 踩过的坑

  1. 第 3 章固定模板是致命缺陷

    • 问题:不读取 memory.goals,无法体现 Q6 的养老目标
    • 修复:添加 goals 字段,第 3 章根据 goals 个性化生成
  2. 第 6 章字符串匹配不稳定

    • 问题:遍历 views 列表做字符串匹配,可能匹配错误或失败
    • 修复:使用 strategy 字段直接读取
  3. Memory 存储结构不完整

    • 问题:只有 views 和 preferences,缺少 goals 和 strategy
    • 修复:添加结构化字段,确保 Q15 能正确读取
  4. 金融计算精度问题

    • 问题:Q7 要求先取整月支出再乘月数,不能在中间步骤四舍五入
    • 修复:使用精确值计算,只在最终结果取整

6.3 Token 消耗 vs 精度权衡

这是比赛中最核心的权衡。Q15 每题 10 分占总分 40%,但 Token 消耗也占 10 分。

方案 Q15 精度 Token 消耗 综合得分
全 LLM 分析 质量高但排名低
全关键词匹配 质量有缺陷但排名高
关键词过滤 + 结构化存储 最优

最终选择了"关键词过滤 + 结构化存储"的混合方案:

  • 快速过滤假设场景(不调 LLM)
  • 只对主观题提取关键信息(不调 LLM,用规则)
  • 结构化存储 goals/strategy(稳定可靠)

最终实现从 8w token 完成 140 道题测评 降低到 5w token。

6.4 对 Agent 开发的启示

  1. 工具调用是 Agent 的核心:ReAct 模式强制工具调用,避免了 LLM 幻觉数值的问题。在实际生产中,这种"数据必须来自工具"的设计理念非常值得借鉴。

  2. Memory 系统需要结构化:简单的 key-value 或列表存储不足以支撑复杂的上下文依赖任务。结构化存储(goals/strategy/product_preferences)让 Q15 的生成逻辑变得简单而可靠。

  3. 假设 vs 事实的区分是 Agent 的基本能力:在金融咨询场景中,"如果客户想要..."和"客户想要..."是完全不同的语义,Agent 必须具备这种区分能力。

  4. 精度和效率的平衡:不是所有场景都需要 LLM。简单的规则匹配(关键词过滤)在特定场景下比 LLM 更高效、更可靠。关键在于找到 LLM 和规则的分界线。

  5. 测试驱动开发:金融计算要求精度(偏离 < 0.1%),必须通过测试验证每个计算步骤。在开发过程中建立了完整的计算测试用例,确保每个公式的正确性。


七、结语

这次比赛让我深刻体会到,构建一个实用的金融 Agent,核心挑战不在于 LLM 的调用能力,而在于:

  • 业务逻辑的精确封装:金融计算必须 100% 准确,不能依赖 LLM 的"大概正确"
  • 状态管理的可靠性:跨题记忆需要结构化存储,而非简单的字符串匹配
  • 工程取舍的智慧:在精度、Token 消耗、响应时间之间找到最优平衡点

最终以 128 分完成比赛。虽然还有提升空间(比如更精细的 Memory 语义提取、更个性化的建议书内容),但这个方案验证了一个核心思路:在金融场景中,Agent 的价值不在于 LLM 的通用能力,而在于业务逻辑的精确工程化实现