招行 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 要求生成包含七个章节的养老规划建议书,这是整场比赛的关键难点:
- 基本情况 — 客户 ID、年龄、性别、风险评级、净资产、收支结构
- 基本假设 — 预期寿命 80 岁、通胀率 2%、退休年龄(延迟退休政策)
- 养老目标 — 客户的退休生活目标
- 退休后财富需求测算 — 总需求、退休金现值、资金缺口
- 产品偏好 — 基于行为数据的偏好分析
- 资产配置方式与具体方案 — 配置策略 + 具体比例
- 其他建议 — 个性化投资建议
3.3 假设 vs 观点:最关键的规则
Q15 的解析中有一条极其重要的规则:Agent 需要正确区分假设场景和真实观点。
| 类型 | 触发词 | 处理方式 |
|---|---|---|
| 假设场景 | 如果、假如、假设 | 不持久化,不影响后续回答 |
| 真实观点 | 想要、认为、希望 | 持久化,影响 Q15 建议书内容 |
具体来说:
- Q12:"如果客户想要追求投资收益最大化" → 这是假设场景,不应该影响 Q15 的资产配置方案
- Q13:"客户想要最小化风险波动" → 这是真实观点,应该影响 Q15 的资产配置方案
- Q6:"客户想要退休后消费水平不下降" → 这是真实观点,应该影响 Q15 的养老目标
这意味着 Agent 必须具备跨题的记忆能力,而且要能正确区分假设和观点。
四、开发框架
4.1 架构选型
采用 LangGraph + ReAct Agent 作为核心框架,原因如下:
- ReAct 模式强制工具调用:Agent 必须先调用工具获取数据,再回答问题,避免 LLM 幻觉数值
- 工具解耦:每种题型对应独立的工具函数,职责清晰
- 状态管理: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.json 和 history.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 调用数据库的形式来获取用户信息,严禁将全量数据拉取到本地"。提供了两个层面的数据库访问:
- 封装查询:
get_customer_info(customer_id)和get_product_action_stats(customer_id) - 自由 SQL:
execute_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 踩过的坑
第 3 章固定模板是致命缺陷
- 问题:不读取 memory.goals,无法体现 Q6 的养老目标
- 修复:添加 goals 字段,第 3 章根据 goals 个性化生成
第 6 章字符串匹配不稳定
- 问题:遍历 views 列表做字符串匹配,可能匹配错误或失败
- 修复:使用 strategy 字段直接读取
Memory 存储结构不完整
- 问题:只有 views 和 preferences,缺少 goals 和 strategy
- 修复:添加结构化字段,确保 Q15 能正确读取
金融计算精度问题
- 问题: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 开发的启示
工具调用是 Agent 的核心:ReAct 模式强制工具调用,避免了 LLM 幻觉数值的问题。在实际生产中,这种"数据必须来自工具"的设计理念非常值得借鉴。
Memory 系统需要结构化:简单的 key-value 或列表存储不足以支撑复杂的上下文依赖任务。结构化存储(goals/strategy/product_preferences)让 Q15 的生成逻辑变得简单而可靠。
假设 vs 事实的区分是 Agent 的基本能力:在金融咨询场景中,"如果客户想要..."和"客户想要..."是完全不同的语义,Agent 必须具备这种区分能力。
精度和效率的平衡:不是所有场景都需要 LLM。简单的规则匹配(关键词过滤)在特定场景下比 LLM 更高效、更可靠。关键在于找到 LLM 和规则的分界线。
测试驱动开发:金融计算要求精度(偏离 < 0.1%),必须通过测试验证每个计算步骤。在开发过程中建立了完整的计算测试用例,确保每个公式的正确性。
七、结语
这次比赛让我深刻体会到,构建一个实用的金融 Agent,核心挑战不在于 LLM 的调用能力,而在于:
- 业务逻辑的精确封装:金融计算必须 100% 准确,不能依赖 LLM 的"大概正确"
- 状态管理的可靠性:跨题记忆需要结构化存储,而非简单的字符串匹配
- 工程取舍的智慧:在精度、Token 消耗、响应时间之间找到最优平衡点
最终以 128 分完成比赛。虽然还有提升空间(比如更精细的 Memory 语义提取、更个性化的建议书内容),但这个方案验证了一个核心思路:在金融场景中,Agent 的价值不在于 LLM 的通用能力,而在于业务逻辑的精确工程化实现。