目标:搭建一个能自动评估交易风险、查询商户信息、给出处置建议的AI风控助手。
从最简单的RAG问答开始,一步步升级到完整的AI Agent
6个阶段 × AI核心能力
| 阶段 | 做什么 | 能学到什么AI能力 | 产出 |
|---|---|---|---|
| Stage 1 | RAG知识库升级(元数据过滤) | RAG深度优化 | 增强版风控知识库 |
| Stage 2 | 风控评估Prompt设计 | Prompt工程、结构化输出 | 风控评估Prompt模板 |
| Stage 3 | 模拟支付API工具调用 | Function Calling原理 | 可调用工具的风控助手 |
| Stage 4 | ReAct多步推理 | Agent架构、决策设计 | 完整风控Agent |
| Stage 5 | 效果评估与指标测量 | 效果评估三层体系 | 评估报告+指标 |
| Stage 6 | 异常处理与成本控制 | 生产化思维 | 生产就绪版 |
前置准备
在终端执行以下命令创建项目目录和知识库文档:
bash
mkdir -p /Users/salar/ai-risk-agent/docs
# ========== 知识库文档 ==========
cat > /Users/salar/ai-risk-agent/docs/交易限额规则.txt << 'EOF'
交易限额规则
## 单笔交易限额
- 借记卡:单笔5万元,单日20万元
- 贷记卡:单笔3万元,单日10万元
- 二维码支付:静态码单笔1000元,动态码单笔50万元
- 跨境交易:单笔等值5万美元,需额外审核
## 限额调整规则
- VIP商户可申请提升单笔限额至20万,需满足月交易额100万以上
- 提升限额需风控审批,审批周期3个工作日
- 临时提额有效期7天,到期自动恢复
## 超限处理
- 超出单笔限额:交易直接拒绝,返回错误码"EXCEED_SINGLE_LIMIT"
- 超出单日限额:当日后续交易拒绝,返回错误码"EXCEED_DAILY_LIMIT"
- 疑似拆分交易:多笔接近限额的交易在1小时内出现,触发风控告警
EOF
cat > /Users/salar/ai-risk-agent/docs/商户风险管理.txt << 'EOF'
商户风险管理
## 风险等级分类
商户风险等级分为A/B/C/D四级:
- A级:低风险,历史无欺诈记录,交易稳定
- B级:一般风险,偶发退款率偏高,需关注
- C级:较高风险,退款率超过15%,或存在投诉集中
- D级:高风险,存在欺诈嫌疑或已被列入黑名单
## 风险等级触发条件
- 退款率超过15%:降级为C级
- 单月投诉超过10笔:降级为C级
- 存在欺诈嫌疑(如虚假交易、套现):降级为D级
- 关联黑名单商户(同法人/同IP):降级为D级
## 各等级管控措施
- A级:正常结算T+1,无特殊限制
- B级:正常结算T+1,大额交易(单笔>5万)需人工复核
- C级:降级为T+1结算(如原为T+0则取消),所有大额交易需人工审核,月度巡检
- D级:暂停收单权限,冻结账户余额,启动调查流程,关联商户同步监控
## 黑名单机制
- 失信商户自动加入黑名单,禁止新增交易
- 黑名单商户关联的法人/联系人同步监控
- 黑名单解除需提交申诉材料,审核周期5个工作日
- 申诉通过后降为C级观察,观察期3个月
EOF
cat > /Users/salar/ai-risk-agent/docs/异常交易识别.txt << 'EOF'
异常交易识别
## 行为类异常
- 同一设备1小时内发起超过50笔交易
- 同一IP地址关联超过10个不同商户号
- 交易金额频繁出现在999、4999、9999等接近限额的整数
- 凌晨2-5点高频交易(正常营业时间外)
- 同一商户1小时内相同金额交易超过5笔
## 资金类异常
- 单日退款金额超过交易额的30%
- 退款后立即在同一商户重新交易
- 大额交易后立即小额交易测试
- 资金快进快出:入账后2小时内转出比例超过80%
## 模式类异常
- 拆分交易:将一笔大额交易拆成多笔小额,规避限额
- 循环交易:A→B→C→A的资金闭环
- 预付卡套现:用信用卡购买预付卡再变现
- 虚假退款:实际未发生退款但系统记录退款
## 告警分级
- 一级告警(黄色):单条规则触发,自动记录,次日人工复核
- 二级告警(橙色):2条以上规则同时触发,实时通知风控人员
- 三级告警(红色):3条以上规则触发或涉及D级商户,立即冻结交易
EOF
cat > /Users/salar/ai-risk-agent/docs/反洗钱规则.txt << 'EOF'
反洗钱规则
## 大额交易报告
- 单日现金交易超过5万元人民币,需向反洗钱中心报告
- 单笔转账超过50万元人民币,需提交大额交易报告
- 跨境交易单笔超过等值1万美元,需提交大额申报
## 可疑交易识别
- 资金快进快出:入账后2小时内转出比例超过80%
- 交易规模与商户经营规模明显不符
- 无真实贸易背景的频繁资金划转
- 利用多个账户分散转入、集中转出
- 频繁更换结算账户
## KYC要求
- 商户入网需完成KYB认证(企业资质审核)
- 个人商户需身份证+人脸识别+银行卡三要素验证
- 企业商户需营业执照+法人身份证+对公账户验证
- 跨境商户需额外提供进出口资质或MSO牌照
## 客户尽职调查
- 高风险客户:每6个月进行一次尽职调查更新
- 中等风险客户:每年进行一次尽职调查更新
- 低风险客户:每2年进行一次尽职调查更新
- 政治公众人物(PEP):需经高级管理层审批后方可建立业务关系
EOF
cat > /Users/salar/ai-risk-agent/docs/结算规则.txt << 'EOF'
结算规则
## 结算周期
- T+0结算:交易日当天完成资金划拨,需支付机构垫资,仅限A级商户
- T+1结算:交易日次日划款到商户账户,为默认结算方式
- D+1结算:按自然日次日结算,不区分工作日和节假日
- T+0升T+1:C级商户自动从T+0降级为T+1
## 结算模式
- 净额结算:对当日所有交易进行轧差,仅划转净差额,适用于收单场景
- 全额结算:每笔交易单独结算,适用于大额交易场景
- 分账结算:平台分账比例上限30%,分账方不超过5个,必须在交易成功后发起
## 对账规则
- 每日T+1自动对账,核对支付通道流水与商户订单
- 差异订单需在3个工作日内完成核查与调账
- 长尾差异(单笔<1元)可按月汇总处理
- 对账失败超过3次自动告警
## 手续费
- 借记卡:0.5%(上限20元)
- 贷记卡:0.6%(无上限)
- 跨境交易附加:0.3%跨境费
- T+0垫资费:0.1%/笔
EOF
Stage 1: RAG知识库升级 --- 元数据让检索更精准
能理解什么
之前RAG实操项目里,文档是"裸"的------检索时只能靠语义相似度。但真实场景中,知识库可能有几百份文档,涵盖不同类别。加元数据(metadata)后,可以先按类别过滤,再做语义检索,大幅提升准确率。
这就是非常理解的:RAG优化不只是算法的事,数据组织方式同样重要。
运行代码
bash
cat > /Users/salar/ai-risk-agent/stage1_rag.py << 'PYEOF'
import os
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings, Document
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.zhipuai import ZhipuAI
Settings.embed_model = HuggingFaceEmbedding(model_name="/Users/salar/bge-small-zh-v1.5")
Settings.llm = ZhipuAI(model="glm-4-flash", temperature=0.1, api_key=os.environ.get("ZHIPUAI_API_KEY"))
# ====== 1. 加载文档并添加元数据 ======
print("正在加载文档...")
doc_dir = "/Users/salar/ai-risk-agent/docs"
raw_docs = SimpleDirectoryReader(doc_dir).load_data()
# 给每个文档添加类别元数据
category_map = {
"交易限额规则": "限额规则",
"商户风险管理": "商户风险",
"异常交易识别": "异常识别",
"反洗钱规则": "反洗钱",
"结算规则": "结算",
}
for doc in raw_docs:
for key, cat in category_map.items():
if key in doc.metadata.get("filename", ""):
doc.metadata["category"] = cat
break
else:
doc.metadata["category"] = "其他"
print(f"加载了 {len(raw_docs)} 个文档")
for doc in raw_docs:
print(f" - {doc.metadata.get('filename', 'unknown')} → 类别: {doc.metadata.get('category')}")
# ====== 2. 分块并保留元数据 ======
splitter = SentenceSplitter(chunk_size=256, chunk_overlap=50)
nodes = splitter.get_nodes_from_documents(raw_docs)
print(f"\n分块后节点数: {len(nodes)}")
for i, node in enumerate(nodes[:3]):
print(f" 节点{i}: 类别={node.metadata.get('category')} | 内容={node.text[:50]}...")
# ====== 3. 构建向量索引 ======
print("\n正在构建向量索引...")
index = VectorStoreIndex(nodes)
# ====== 4. 对比:无过滤 vs 手动过滤 ======
test_queries = [
"商户单笔交易限额是多少",
"退款率高的商户怎么处理",
"什么是拆分交易",
]
print("\n" + "="*60)
print("【对比测试:无过滤检索 vs 按类别过滤后检索】")
print("="*60)
for query in test_queries:
print(f"\n{'─'*50}")
print(f"❓ 问题: {query}")
# 无过滤:纯语义检索
retriever = index.as_retriever(similarity_top_k=3)
all_nodes = retriever.retrieve(query)
print(f"\n [无过滤] Top3 来源类别: {[n.metadata.get('category') for n in all_nodes]}")
print(f" Top1 相似度: {all_nodes[0].score:.4f} | 内容: {all_nodes[0].text[:80]}...")
# 手动过滤:只保留相关类别的结果
# 先判断这个问题最相关哪个类别
query_category_map = {
"限额": "限额规则",
"退款": "商户风险",
"拆分": "异常识别",
}
target_cat = None
for kw, cat in query_category_map.items():
if kw in query:
target_cat = cat
break
if target_cat:
filtered_nodes = [n for n in all_nodes if n.metadata.get("category") == target_cat]
if filtered_nodes:
print(f" [过滤后] 只保留类别={target_cat},共{len(filtered_nodes)}条")
print(f" Top1 相似度: {filtered_nodes[0].score:.4f} | 内容: {filtered_nodes[0].text[:80]}...")
else:
print(f" [过滤后] 类别={target_cat} 无结果,需放宽过滤条件")
else:
print(f" [过滤后] 无法自动判断类别,回退到无过滤")
# ====== 5. 生成答案对比 ======
print("\n\n" + "="*60)
print("【最终答案对比:有没有元数据引导,答案质量差多少】")
print("="*60)
query = "退款率超过15%的商户会怎样"
print(f"\n❓ 问题: {query}")
# 用完整知识库回答
engine_all = index.as_query_engine(similarity_top_k=3)
resp_all = engine_all.query(query)
print(f"\n[全库检索回答]\n{resp_all}")
# 只用商户风险相关文档回答
merchant_nodes = [n for n in nodes if n.metadata.get("category") == "商户风险"]
if merchant_nodes:
merchant_index = VectorStoreIndex(merchant_nodes)
engine_filtered = merchant_index.as_query_engine(similarity_top_k=3)
resp_filtered = engine_filtered.query(query)
print(f"\n[过滤后检索回答]\n{resp_filtered}")
print("\n" + "="*60)
print("💡 思考:两个回答有什么区别?过滤后的回答是否更聚焦、更少无关信息?")
print("💡 这就是元数据的价值------在检索前缩小范围,减少噪音干扰。")
print("💡 在生产环境中,知识库可能有几百份文档,元数据过滤是必需品而非锦上添花。")
PYEOF
bash
cd /Users/salar/ai-risk-agent && python stage1_rag.py
跑完之后对比数据,会有不少的差异
- 无过滤检索时,Top3结果里会有来自不相关类别的(比如问限额,结果里混进了结算规则)
- 过滤后的答案更精准,也没有丢失信息。
- 目前测试的知识库只有一点点数据,如果知识库从5份变成500份,元数据过滤的价值会很大。
元数据过滤三大落地价值(结合本次测试印证)
- 降噪:从源头屏蔽跨业务无关文档,避免结算、反洗钱内容干扰限额 / 商户风险类问题;
- 聚焦:生成回答只保留对应分类核心信息,减少额外无关业务条款,回答简洁精准;
- 性能优化:参与向量计算的节点变少,知识库越大,检索耗时优化越明显。
那结合着之前RAG实操的,整体的流程就完整了
- 文件 → unstructured 解析文本 → LlamaIndex 语义分块 → 人工自定义绑定 metadata (category 等) → LlamaIndex 调用 sentence-transformers 生成向量 → 写入 Qdrant 向量库 → 检索时元数据过滤 + 向量相似度召回 → 组装上下文给大模型输出答案