ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DeepSeek驱动的信用卡欺诈实时检测:行为上下文与异常特征提取实践

DeepSeek驱动的信用卡欺诈实时检测:行为上下文与异常特征提取实践 简介这是一份以DeepSeek大模型技术为主线、面向银行信用卡实时欺诈检测的完整技术方案文档。内容覆盖交易数据特征体系构建、结构化与非结构化数据预处理、时序Transformer编码、离群点检测与异常特征提取、弱监督自动标注与样本平衡、多任务学习损失设计以及LoRA与Adapter参数高效微调、超参数网格搜索、模型蒸馏目标与蒸馏数据集构建等关键环节基本串起从数据治理到模型部署的工程链路。文档共236页含51个大章节支持目录章节跳转和阅读器左侧书签定位包体为单个PDF文件压缩包大小11.11MB文字、图表、目录均显示正常。目前已有69人学习。对于风控算法工程师、数据科学家和金融科技研究者而言既可作为技术选型与方案规划的参考也能为反欺诈模型训练、微调和蒸馏落地提供具体思路。1. 为什么信用卡欺诈实时检测开始用 DeepSeek大模型补的是“行为上下文”这块短板同一张卡 40 分钟前在城东便利店刷过 28 元现在在城西金店刷 12800 元规则引擎大概率会拦截但真正让风控头疼的是“看起来正常”的盗刷——金额不大、商户常见、时间也对只是行为链条里多了一丝不合理。DeepSeek 进入信用卡欺诈实时检测后交易行为模式识别从“单笔特征超限”变成了“整段行为轨迹的语义判断”异常特征提取算法也从固定阈值演化为可解释的推理输出。这篇笔记把选型理由、特征窗口、提示词设计、异常评分、部署避坑和验证方法串成一条能直接落地的路径适合正在评估方案或已经进入开发的算法工程师与风控架构师参考。2. 方案拆解为什么是 DeepSeek而不是只靠规则引擎2.1 规则引擎、统计模型与大模型的分工边界传统反欺诈系统里规则引擎是绝对主力单笔金额超限、短时同卡多笔、高风险商户码命中这类判断延迟低、可解释性强生产环境必须继续保留。统计模型负责给每笔交易打一个数值分XGBoost、逻辑回归和孤立森林各管一摊捕捉“金额分布异常”“频次突增”这类数值型偏离。这两个老伙计有一个共同短板它们看到的是一行特征不是一段行为。模型不知道“用户上个月天天在便利店今天突然出现一笔金额不大的便利店消费”有什么问题因为单看金额和商户类型这笔交易跟历史记录没有统计性差异。大模型补的正是这块。把近 N 笔交易、设备、位置、商户类型拼成一段结构化文本让模型判断“这笔消费是否符合该用户的历史行为模式”交易行为模式识别实质上从一个分类问题变成了一个语义判断题。DeepSeek 在长上下文理解和推理上表现突出而且支持私有化部署对银行这种数据不能出域的行业这一点是硬门槛。所以我的选型结论是规则引擎做第一道闸统计模型做批量评分大模型做“规则覆盖不到的长尾行为判断”。三者不是替代关系而是并联后融合。有些团队一上来就要用大模型替换全部规则这是最常翻车的思路——模型偶发不稳定而规则引擎永远稳定。2.2 实时检测方案的整体数据流与组件清单常见做法是让交易事件先经过消息队列削峰再进特征服务窗口计算拿到上下文后拼提示词、调模型最后进融合决策。组件不要贪多四段足够。我会在本地项目里长期维护下面这张清单组件常见选型职责交易事件接入Kafka / Pulsar接收渠道实时交易流缓冲并削峰用户行为窗口Redis 滑动窗口计算保存每张卡最近 20~50 笔交易摘要特征序列构造Python 服务把窗口数据转成模型可读的 JSON 行为轨迹大模型推理DeepSeek API / 私有化部署输出风险等级、异常特征、拦截建议融合决策Python / Drools大模型分、统计模型分、规则命中三者加权结果回流数据库 消息队列回写判定结果供离线回测与样本积累这个结构里最容易被低估的是“用户行为窗口”。有的团队把全部历史交易塞进提示词延迟和成本双双失控。窗口控制在 20~50 笔以内既覆盖“连续试探”这类行为链条又不会把上下文撑爆。窗口数据不要离线算好一次性灌入而要在线实时更新否则上一笔判断结果还没写回下一笔就拿不到最新行为。2.3 DeepSeek 在实时链路里的定位在线推理、离线挖掘与私有化部署DeepSeek 在这个方案里不只是“在线打分模型”。我一般把它放在三个位置各司其职。第一个位置是在线推理交易到达后构造行为上下文调用模型接口返回风险判断。这个场景对延迟敏感必须控制单次输入长度和并发数建议单独开一组实例承载不要和内部办公 AI 混用。第二个位置是离线挖掘每天把被规则放过但最终确认欺诈的样本捞出来让 DeepSeek 复盘“当初哪一步异常被忽略了”把挖掘出的新特征补充进提示词和特征工程。第三个位置是私有化部署数据不出行是硬要求时用 vLLM 或 Ollama 在内网拉起 DeepSeek 服务业务侧通过 OpenAI 兼容接口调用。Dify 这类工作流编排平台适合做运营 Demo但我不建议让它承载交易级实时调用多一层转发就多一层延迟。一句话概括选型逻辑大模型不是替代规则而是看见规则看不见的事。这套方案能不能落地很大程度上取决于你有没有把这三个位置切分清楚——全堆在在线环节迟早被延迟和成本拖垮。3. 交易行为模式识别把流水变上下文提示词与特征工程3.1 时间窗口怎么切滑动窗口与关键特征字段交易行为模式识别的第一步是把原始流水变成一段“行为轨迹”。我惯用的窗口策略是以当前交易为终点向前取同一张卡最近 30 笔交易再叠加三个辅助窗口——最近 10 分钟交易频次、最近 24 小时累计金额、同一设备最近 5 笔交易的商户分布。特征字段选择按优先级排卡号脱敏保留后四位加上持卡人历史常用城市取近 30 笔众数当前交易的金额、商户类别码MCC、城市、时间、设备指纹、渠道近 30 笔里是否出现过相同 MCC、相同城市、相同设备最近一笔交易与本笔的时间间隔近 10 分钟同卡交易次数这几个字段足够拼出一个有语义的行为轨迹。拿这些字段去拼提示词而不是把原始流水原样倒进去。原始流水噪声大还容易把上下文长度推到不可控。设备指纹这个字段尤其关键很多欺诈行为在商户和金额上伪装得很好但设备一换就露馅。3.2 提示词模板与 few-shot 示例的 5 个固定部分提示词质量直接决定输出可用性。我使用的模板固定包含五块角色定义、任务定义、输入数据、输出格式、few-shot 示例。角色定义必须写清楚“你是银行信用卡反欺诈风控专家负责判断当前交易是否符合持卡人历史行为模式”。任务定义里明确三个输出项风险等级低/中/高、异常特征列表、拦截建议。输出格式强制 JSON方便代码解析。few-shot 示例给两三组就行正例用“持卡人通勤路线上的同商户消费”负例用“跨城市加短时多次小额试探”。示例不要贪多先验证模型能不能稳定跟从格式再逐步补充业务样本。提示词里还要加一句硬约束“如果信息不足请输出低风险不要猜测”这能明显降低模型瞎编的比例。3.3 构造交易上下文的 Python 代码下面的代码负责把窗口数据压成结构化的行为轨迹import json from datetime import datetime def build_txn_context(card_id, current_txn, recent_txns, window_meta): 构造交易行为模式识别的输入上下文 :param card_id: 脱敏卡号如 6222****1234 :param current_txn: 当前交易字典 :param recent_txns: 该卡最近30笔交易列表 :param window_meta: 辅助窗口统计如最近10分钟频次 :return: 结构化的上下文 dict # 从近30笔里统计常用城市与常用MCC作为“历史习惯” cities [t[city] for t in recent_txns if t.get(city)] mccs [t[mcc] for t in recent_txns if t.get(mcc)] usual_city max(set(cities), keycities.count) if cities else 未知 usual_mcc max(set(mccs), keymccs.count) if mccs else 未知 # 当前交易是否与习惯一致 same_city 1 if current_txn.get(city) usual_city else 0 same_mcc 1 if current_txn.get(mcc) usual_mcc else 0 # 与上一笔交易的时间间隔判断“异常提速” last_gap_minutes None if recent_txns: last_ts recent_txns[-1].get(txn_time) if last_ts: fmt %Y-%m-%d %H:%M:%S last_gap_minutes (datetime.strptime(current_txn[txn_time], fmt) - datetime.strptime(last_ts, fmt)).total_seconds() / 60 context { card_id: card_id, current_txn: { amount: current_txn.get(amount), mcc: current_txn.get(mcc), city: current_txn.get(city), device_id: current_txn.get(device_id), txn_time: current_txn.get(txn_time), channel: current_txn.get(channel, online), }, history_summary: { usual_city: usual_city, usual_mcc: usual_mcc, recent_txn_count: len(recent_txns), recent_10min_count: window_meta.get(recent_10min_count, 0), recent_24h_amount: round(window_meta.get(recent_24h_amount, 0.0), 2), same_city: same_city, same_mcc: same_mcc, last_txn_gap_minutes: round(last_gap_minutes, 1) if last_gap_minutes else None, }, # 只保留最近5笔原文给模型“连续动作”的感知 recent_5_txns: [ {amount: t.get(amount), mcc: t.get(mcc), city: t.get(city), txn_time: t.get(txn_time)} for t in recent_txns[-5:] ], } return context这段代码的逻辑是先把近 30 笔压成少数统计量再保留最近 5 笔原文。模型对连续动作的感知比对长列表的感知更敏锐。参数说明recent_10min_count捕“批量试探”recent_24h_amount捕“大额集中”last_txn_gap_minutes判断“异常提速”这三个字段在后续模型输出里的权重最高。3.4 输出契约测试先验证再上联调提示词定了输出格式定了开发时一定要先做一次输出契约测试。我会拿 100 条历史已标记交易跑一遍确认风险等级、异常特征、拦截建议三个字段都能稳定解析成合法 JSON再进联调。这个阶段常见的问题是模型偶尔在 JSON 里夹解释性文字。解析代码要做好容错先尝试json.loads失败后用正则提取第一个{到最后一个}之间的片段再解析。这个容错逻辑在模型升级后尤其重要模型一换版本输出格式可能漂移没有容错就是线上事故。4. 异常特征提取算法从“像不像”到可量化的异常分4.1 大模型输出解析与风险等级归一化大模型输出“低风险”或“高风险”还不够实时检测系统需要一个数值分数和规则引擎融合。这里的关键是把模型输出的三个字段映射成分数。风险等级映射低0中0.5高1.0。异常特征列表用于加分每命中一条加 0.1但封顶 0.5防止模型为了凑异常而无上限加分。拦截建议字段则作为决策引擎的参考信号。解析代码import json import re def parse_model_risk(raw_text): 从大模型输出中解析风险等级、异常特征与拦截建议 容错处理优先标准JSON解析失败后用正则截取 if not raw_text: return {risk_level: unknown, features: [], action: pass, score: 0.0} # 1. 先尝试标准 JSON 解析 try: data json.loads(raw_text) except json.JSONDecodeError: # 2. 模型偶尔夹带解释文字用正则在 JSON 片段 match re.search(r\{.*\}, raw_text, re.S) if not match: return {risk_level: unknown, features: [], action: pass, score: 0.0} try: data json.loads(match.group()) except json.JSONDecodeError: return {risk_level: unknown, features: [], action: pass, score: 0.0} level data.get(risk_level, 未知) features data.get(abnormal_features, []) action data.get(suggested_action, pass) # 3. 风险等级映射为基础分 base {低: 0.0, 中: 0.5, 高: 1.0}.get(level, 0.0) # 4. 每条异常特征加 0.1封顶 0.5 bonus min(0.1 * len(features), 0.5) score min(base bonus, 1.0) return { risk_level: level, features: features, action: action, llm_score: round(score, 3), }这个解析逻辑的取舍在于模型分被钳制在 0~1特征数惩罚封顶。建议把“高风险且特征数大于 2”作为强制进人工审核的触发条件不要直接拦截。4.2 与统计模型融合孤立森林与 z-score 偏差单独靠大模型打分不稳。我常把大模型分与统计模型分按权重融合。统计侧用孤立森林对同一特征向量跑异常分再与模型分做加权。孤立森林适合欺诈检测的原因是不用假设数据分布对高维稀疏特征有天然优势。实用融合公式def fused_score(llm_score, iso_score, rule_hit_count, w_llm0.5, w_iso0.3, w_rule0.2): 融合大模型分、孤立森林分与规则命中数 llm_score: 0~1来自上一步的模型解析 iso_score: 0~1孤立森林归一化异常分 rule_hit_count: 规则命中条数 rule_score min(rule_hit_count / 3.0, 1.0) # 命中3条封顶 fused w_llm * llm_score w_iso * iso_score w_rule * rule_score return round(min(fused, 1.0), 3)权重不要拍脑袋。我的做法是用离线回测数据集跑网格搜索以“固定误报率下的召回率”为目标函数选一组权重。回测时重点看两组权重差异一组 w_llm 偏高一组 w_iso 偏高对比它们在“突增型”和“长尾型”两类欺诈上的表现。突增型靠统计模型就能抓长尾型必须靠大模型权重选择本质是在做业务偏好的取舍。4.3 实时评分的完整调用链与超时控制完整链路是交易到达 → 查 Redis 窗口 → 拼上下文 → 调 DeepSeek → 解析分数 → 融合规则 → 返回决策。实时场景必须给每一步设超时否则会拖垮交易系统。import time from openai import OpenAI def score_a_txn(txn, recent_txns, window_meta, client): 单笔交易评分入口 返回 (fused_score, detail) context build_txn_context(txn[card_id], txn, recent_txns, window_meta) prompt build_prompt(context) # 按 3.2 节模板拼装 t0 time.time() try: resp client.chat.completions.create( modeldeepseek-chat, # 以你实际开通的模型名为准 messages[ {role: system, content: 你是银行信用卡反欺诈风控专家……}, {role: user, content: prompt}, ], temperature0.1, max_tokens256, timeout2.0, # 关键参数大模型调用超时 ) raw resp.choices[0].message.content except Exception as e: # 超时或异常走兜底只按统计模型和规则打分 latency_ms int((time.time() - t0) * 1000) return _fallback_rule_score(txn), {error: str(e), latency_ms: latency_ms} latency_ms int((time.time() - t0) * 1000) parsed parse_model_risk(raw) iso_score _iso_forest_score(txn) # 统计侧分数 rule_hits _count_rule_hits(txn) # 规则命中条数 fused fused_score(parsed[llm_score], iso_score, rule_hits) return fused, {llm_latency_ms: latency_ms, details: parsed}提示temperature 我固定在 0.1。风险判断任务要确定性不要鼓励模型自由发挥。max_tokens 设 256 足够因为输出是短 JSON设长了平白增加等待。超时时间给 2 秒如果推理服务排队宁可降级也不阻塞交易链路。4.4 一个反直觉参数最近 5 笔比最近 30 笔更关键做交易行为模式识别时我做过一组对照。只给最近 5 笔交易原文加 30 笔统计摘要与完整给 30 笔原文相比模型对“突发性异常”的识别率几乎无差别但对“渐进式异常”——逐笔加大金额、逐步换城市——明显更依赖全量摘要。原因是模型对连续动作的上下文感知强于对长列表的复杂推理。现在我的模板固定为历史摘要用 30 笔计算统计量正文只保留最近 5 笔原文。这样既保住语义又把 token 消耗压到最低。token 消耗在实时链路上是纯成本不能不算。5. 实时检测部署的 4 个高发坑与排查记录延迟、超时、上下文与并发5.1 现象大模型调用延迟从 300ms 涨到 2s交易网关持续报警原因并发量上来后DeepSeek API 侧排队单次调用又带着越来越长的历史上下文响应时间被双重拉高。这是实时检测方案里最容易先爆的雷。解决给每笔交易的行为上下文做预算控制限制 history_summary 字段数量和 recent_5_txns 的嵌套层级。再把超过 2 秒的调用降级为统计模型分不阻塞主流程。如果走 vLLM 私有化部署建议给推理服务单独开一组 GPU 实例与在线交易链路隔离避免相互抢资源。5.2 现象模型返回“低风险”但人工复核发现明显异常原因提示词里少了关键特征字段最常见的就是漏掉设备指纹。很多欺诈是设备维度而非金额维度只给了商户和城市模型不知道“这台设备从来没出现过”自然就会误判。解决把设备指纹、登录 IP 归属地、浏览器指纹纳入特征工程。设备变更加金额适中加城市变化三个组合出现时模型会正确输出“高风险”。这个坑我踩过三次最后把设备字段写进了特征窗口的第一优先级。5.3 现象交易事件重复投递同一笔被模型扣了两次“高风险”原因消息队列消费端没有做幂等。实时链路里 Kafka 的 at-least-once 语义会重复推送消费端无条件调用大模型既浪费 token又导致同一笔交易两次风险分不一致业务侧怎么解释都讲不通。解决在消费端维护一个“交易流水号到评分结果”的幂等表Redis 里设置五分钟 TTL 足够。命中的直接返回缓存分数不再重复调用大模型。这个幂等逻辑必须在联调前就写好否则上线第一周就会出乱子。5.4 现象上线一周后误报率从 1.2% 爬到 3%业务被投诉原因阈值固定加特征分布漂移。欺诈团伙发现规则后调整攻击方式导致模型输出分布整体右移固定阈值跟着失效。这类问题最难排查因为它不是单点故障而是系统性漂移。解决把阈值改成动态。我一般用 EWMA 对最近一小时的平均分做平滑阈值取“均值加二倍标准差”。每天凌晨跑一次离线校准任务更新阈值参数。大模型方案里这个动作尤其重要提示词写得再好阈值过期一样翻车。6. 验证与进阶离线回测、分级处置与提示词持续喂养方案再完整最终也得靠离线回测建立信心。我常用的做法是把过去 90 天的历史交易按时间顺序回放用同样的特征窗口和大模型调用逻辑给每笔打分算三组指标固定误报率下的召回率、单笔平均延迟、规则引擎与大模型的重合命中率。回测有个硬约束不能用未来数据构造窗口每笔交易只能用发生时刻之前的历史行为否则指标虚高上线立刻现原形。上线之后的进阶动作是分级处置不要只做“拦放”二值决策。我把结果分成四档通过、观察、人工审核、直接拦截。观察档很有价值因为大模型能给出自然语言理由业务侧拒付时可以直接引用“该卡近期无此设备登录记录且商户类型与历史习惯不一致”这是规则引擎给不了的。提示词要像模型一样持续迭代。我现在保留一个固定习惯——每周五下午把本周漏网的欺诈样本整理成新的 few-shot 示例补进模板同时把被误杀的正常交易也整理成反例。这个动作最朴素但回报最高持续三个月后模型对“午后咖啡店消费”这类伪装型场景的判断明显变准。如果团队有余力再拿积累的样本去微调一个小的专用分类器跟 DeepSeek 并联打分。最后说一句我交付项目时踩过的最重的一脚坑不要让大模型直接拦截交易。它的分数永远只是决策引擎的输入不是决策本身。我最早贪图省事让模型直接放行“低风险”交易结果漏过一批团伙作案被业务部门点名复盘。后来所有方案里我都保留统计模型兜底和人工复核通道拦截的最终决定权永远留给规则与人工。模型可以很聪明但风控必须留得住后手。希望这些落地经验能帮到你少走我走过的弯路。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进