从0到1搭建AI智能支付风控助手Stage4-Agent编排 — 让AI自己思考、决策、行动

你要学什么

前一步是"人告诉AI该不该调工具",这一步让AI自己规划行动步骤。这就是Agent的核心------不是被动的工具调用者,而是能自主决策的执行者。

这一阶段有两个版本,建议都了解:

版本 方式 可靠性 用途
纯文本版 让LLM输出JSON,用正则解析 低(容易跑偏) 理解原理、面试说清"为什么不行"
原生Tool Calling版 用SDK原生接口,模型输出结构化tool_calls 高(100%可靠) 生产环境标准做法

💡 踩坑经验 :Stage 3 你已经体验过纯文本解析的不稳定性------LLM 经常把 JSON 包在代码块里、前面加解释文字、甚至截断。生产环境绝对不能靠正则抠 JSON,必须用原生 Tool Calling。

ReAct 模式核心思想:Observe(观察)→ Think(思考)→ Act(行动)→ Observe(观察新信息)→ ... 循环直到得出结论。

运行代码(原生 Tool Calling 版)

bash 复制代码
cat > /Users/salar/ai-risk-agent/stage4_agent.py << 'ENDOFSCRIPT'
import os
import json
import re
from zhipuai import ZhipuAI

# 使用原生 zhipuai SDK,tool calling 更可靠
client = ZhipuAI(api_key=os.environ.get("ZHIPUAI_API_KEY"))

# ====== 1. 工具定义 ======
def query_merchant(merchant_id: str) -> dict:
    """查询商户基本信息,包括等级、行业、日均交易额等"""
    db = {
        "M001": {"id": "M001", "name": "阳光百货", "level": "A", "category": "零售", "avg_daily_amount": 50000, "max_single_tx": 20000},
        "M002": {"id": "M002", "name": "星辰数码", "level": "C", "category": "数码3C", "avg_daily_amount": 8000, "max_single_tx": 5000, "refund_rate": "18%"},
        "M003": {"id": "M003", "name": "优选生活", "level": "B", "category": "生活服务", "avg_daily_amount": 15000, "max_single_tx": 10000},
        "M999": {"id": "M999", "name": "风险商户", "level": "D", "category": "未知"},
    }
    return {"merchant": db.get(merchant_id, {"id": merchant_id, "name": "未知商户"})}

def query_transaction_history(merchant_id: str) -> dict:
    """查询商户近7天历史交易记录"""
    data = {
        "M001": {"total_tx": 126, "total_amount": 352000, "avg_amount": 2793, "night_tx_ratio": "2%", "refund_count": 3},
        "M002": {"total_tx": 45, "total_amount": 180000, "avg_amount": 4000, "night_tx_ratio": "35%", "refund_count": 12},
        "M003": {"total_tx": 89, "total_amount": 67000, "avg_amount": 753, "night_tx_ratio": "8%", "refund_count": 5},
    }
    return {"history": data.get(merchant_id, {"total_tx": 0, "total_amount": 0})}

def check_blacklist(merchant_id: str) -> dict:
    """检查商户是否在风控黑名单中"""
    blacklist = {"M999": "涉嫌洗钱", "M666": "欺诈交易"}
    reason = blacklist.get(merchant_id)
    return {"in_blacklist": reason is not None, "reason": reason}

TOOL_FUNCTIONS = {
    "query_merchant": query_merchant,
    "query_transaction_history": query_transaction_history,
    "check_blacklist": check_blacklist,
}

# 工具定义(OpenAI Function Calling 格式,智谱兼容)
TOOLS_DEF = [
    {
        "type": "function",
        "function": {
            "name": "query_merchant",
            "description": "查询商户基本信息,包括等级、行业、日均交易额、单笔限额等",
            "parameters": {
                "type": "object",
                "properties": {
                    "merchant_id": {"type": "string", "description": "商户ID,如M001"}
                },
                "required": ["merchant_id"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "query_transaction_history",
            "description": "查询商户近7天历史交易记录,包括笔数、金额、夜间占比、退款数等",
            "parameters": {
                "type": "object",
                "properties": {
                    "merchant_id": {"type": "string", "description": "商户ID,如M001"}
                },
                "required": ["merchant_id"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "check_blacklist",
            "description": "检查商户是否在风控黑名单中",
            "parameters": {
                "type": "object",
                "properties": {
                    "merchant_id": {"type": "string", "description": "商户ID,如M001"}
                },
                "required": ["merchant_id"]
            }
        }
    },
]

# ====== 2. ReAct Agent ======

SYSTEM_PROMPT = """你是专业的支付风控Agent。评估交易风险。

评估流程:
1. 分析已知信息,判断还缺什么数据
2. 调用工具获取更多信息(可以多次调用)
3. 信息足够后,给出最终风险评估

风险等级:低、中、高、极高

最终评估用以下JSON格式输出(放在代码块中):
{
  "risk_level": "高",
  "risk_factors": ["因素1", "因素2"],
  "action": "人工审核",
  "confidence": 0.9,
  "reasoning": "推理过程"
}

信息不足就继续调用工具,不要瞎猜。"""

def run_react_agent(transaction: str, max_steps: int = 6) -> dict:
    """ReAct Agent 主循环,使用原生 tool calling"""
    
    # ⚠️ 重要:messages 全部用纯 dict 格式,不能把 SDK 返回的 Pydantic 对象直接塞回去
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": f"请评估以下交易的风险:\n{transaction}"}
    ]
    
    trace = []
    
    for step in range(max_steps):
        print(f"\n  🧠 Step {step+1}: 思考中...")
        
        response = client.chat.completions.create(
            model="glm-4-flash",
            messages=messages,
            tools=TOOLS_DEF,
            temperature=0.1,
            max_tokens=1024,
        )
        
        msg = response.choices[0].message
        content = msg.content or ""
        tool_calls = msg.tool_calls
        
        trace.append({"step": step+1, "role": "assistant", "content": content[:300]})
        
        # 情况1:模型想调用工具
        if tool_calls and len(tool_calls) > 0:
            # 构造纯 dict 格式的 assistant 消息(带 tool_calls)
            assistant_msg = {
                "role": "assistant",
                "content": content,
                "tool_calls": []
            }
            for tc in tool_calls:
                assistant_msg["tool_calls"].append({
                    "id": tc.id,
                    "type": "function",
                    "function": {
                        "name": tc.function.name,
                        "arguments": tc.function.arguments
                    }
                })
            messages.append(assistant_msg)
            
            # 执行每个工具调用,返回 tool 消息
            for tc in tool_calls:
                tool_name = tc.function.name
                try:
                    tool_args = json.loads(tc.function.arguments)
                except:
                    tool_args = {}
                
                print(f"  📞 调用: {tool_name}({tool_args})")
                
                if tool_name in TOOL_FUNCTIONS:
                    result = TOOL_FUNCTIONS[tool_name](**tool_args)
                else:
                    result = {"error": f"未知工具 {tool_name}"}
                
                observation = json.dumps(result, ensure_ascii=False)
                print(f"  📥 结果: {observation[:200]}")
                
                messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": observation
                })
                trace.append({"step": step+1, "tool": tool_name, "result": observation[:200]})
            
            continue
        
        # 情况2:模型给出文本回答,尝试提取结论
        print(f"  💬 {content[:200]}")
        messages.append({"role": "assistant", "content": content})
        
        # 提取 JSON 结论
        conclusion = None
        m = re.search(r'```json\s*\n?(.*?)```', content, re.DOTALL)
        if m:
            try:
                conclusion = json.loads(m.group(1).strip())
            except:
                pass
        if not conclusion:
            m = re.search(r'\{[^{}]*"risk_level"[^{}]*\}', content, re.DOTALL)
            if m:
                try:
                    conclusion = json.loads(m.group(0))
                except:
                    pass
        
        if conclusion and "risk_level" in conclusion:
            print(f"  ✅ 得出结论")
            conclusion["trace"] = trace
            conclusion["steps"] = step + 1
            return conclusion
    
    return {"risk_level": "无法判断", "reason": "超过最大步数", "trace": trace, "steps": max_steps}

# ====== 3. 测试 ======
print("="*60)
print("【ReAct Agent测试:原生Tool Calling版】")
print("="*60)

test_cases = [
    {"desc": "可疑交易", "info": "商户M002在凌晨3点发起3笔大额贷记卡交易,金额分别为28000/27500/29800元"},
    {"desc": "疑似拆分交易", "info": "商户M003在凌晨2点连续5笔4999元交易,每笔间隔2分钟"},
    {"desc": "正常交易", "info": "商户M001在上午10点发起一笔500元借记卡交易"},
    {"desc": "黑名单商户", "info": "商户M999发起一笔10000元交易"},
]

for case in test_cases:
    print(f"\n{'═'*50}")
    print(f"💳 场景: {case['desc']}")
    print(f"   交易: {case['info']}")
    
    result = run_react_agent(case["info"])
    
    print(f"\n   📋 最终评估:")
    print(f"   - 风险等级: {result.get('risk_level', '未知')}")
    if "risk_factors" in result:
        print(f"   - 风险因素: {result['risk_factors']}")
    if "action" in result:
        print(f"   - 建议措施: {result['action']}")
    if "confidence" in result:
        print(f"   - 置信度: {result['confidence']}")
    print(f"   - 决策步数: {result.get('steps', '?')}")

print("\n" + "="*60)
print("💡 学习要点:")
print("  1. 原生 tool calling:模型直接输出结构化 tool_calls,格式100%可靠")
print("  2. messages 必须是纯 dict:SDK 返回的 Pydantic 对象不能直接塞回去")
print("  3. ReAct循环:思考→调用→观察→再思考,直到信息足够")
print("  4. 面试题:为什么不用纯文本解析?(不可靠、成本高、容易跑偏)")
print("  5. 每调用一次工具就是一次API调用,要考虑Token成本和延迟")
ENDOFSCRIPT
bash 复制代码
cd /Users/salar/ai-risk-agent && python stage4_agent.py

跑完后想一想

  1. 高风险交易和正常交易,Agent 的决策步数有差异吗?为什么?
  2. Agent 先查什么工具?为什么是这个顺序?这和真实风控人员的思路一致吗?
  3. Agent 比单次 LLM 调用好在哪?代价是什么?(Token 消耗、延迟)
  4. 什么场景适合用 Agent?什么场景用一次 LLM 调用就够了?
  5. 如果 Agent 在 Step 1 调错了工具,后续能自我修正吗?

Stage 3 和Stage 4,手动 Function Calling vs 原生 SDK ReAct Agent的差异

一、先分清两段代码对应的两种模式

  1. 上一段 LlamaIndex 手动循环版(人 / 代码控制工具逻辑) 你之前那段混合 RAG 的代码:function_calling_loop
  2. 当前纯智谱 SDK 原生 Tool Calling ReAct Agent(AI 自主规划行动) 现在这份无 LlamaIndex、原生zhipuai的代码

二、核心区别:谁来判断「要不要调用工具、调用哪个、调用几次」

  1. 旧手动版:代码强制给固定工具列表,AI 只能被动选择,无自主思考链条
ini 复制代码
# 旧版关键逻辑:代码把全部工具硬塞给prompt文本
def build_tool_prompt(...):
    tool_desc = "\n".join([所有工具写死进字符串])
    prompt = 固定模板 + 工具文本 + 交易信息

本质缺陷: 工具判断权不在大模型,在 Prompt 模板约束

你提前写死规则:要么输出{"tool":xxx},要么输出风险 JSON; AI 只是按你规定的文本格式填空,没有独立思考链路。

无分层思考、无多轮自主规划

每一轮代码无脑重新构造完整 Prompt,上下文只是简单拼接字符串;

AI 不会自己意识到**「我缺商户基础信息→先查商户→查到发现退款率高→再查交易历史→再查黑名单」这种分步逻辑**。

举例子:M002 场景

手动版第一轮只能随机选一个工具; 查完商户后,代码不会引导模型主动发起第二轮查询,全靠 prompt 文本提示,很容易一轮直接输出结论漏数据。 工具调用格式靠文本正则约束,极不稳定 靠字符串匹配提取 JSON,模型经常夹带多余文字、换行、解释,直接解析报错;

没有 SDK 原生结构化tool_calls字段。 没有完整 ReAct 思维链 ReAct = Reason (思考) → Act (工具行动) → Observe (观察结果) → 循环 手动版只是简单「提问→返回工具文本→拼接上下文再提问」,没有标准对话消息结构区分 system/user/assistant/tool 角色。

2)新版原生 Tool Calling ReAct Agent:大模型完全自主规划行动(Agent 核心体现) 关键代码证明 AI 拥有自主决策权

  1. 工具通过SDK标准tools参数传入,不是塞进prompt文本
ini 复制代码
response = client.chat.completions.create(
    model="glm-4-flash",
    messages=messages,
    tools=TOOLS_DEF,  # SDK原生工具注册,模型原生识别工具能力
    temperature=0.1,
)
  1. 模型自主输出结构化tool_calls(不是文本JSON)
ini 复制代码
msg = response.choices[0].message
tool_calls = msg.tool_calls  # SDK原生字段,模型主动决定要调用工具

三、一句话总结两者定位区别

旧手动 Function Calling :增强版问答机器人(代码)规定好所有工具、输出格式,AI 只是根据模板被动选择工具,没有自主分步推理能力,属于工具增强 LLM,不是 Agent。

新版 ReAct 原生 Tool Calling:自主决策 AgentAI 拥有完整思考闭环,自主判断缺什么数据、按什么顺序调用工具、什么时候停止推理输出结论;具备规划、观察、迭代反思能力,这是Agent的核心特质。

跑完过程中遇到的问题

JSON解析失败

Stage 4 跑起来了但 JSON 解析老失败。这是个很典型的问题------LLM 喜欢把 JSON 包在 json 代码块里,还爱加前面的"思考文字",导致解析器读不出来。

问题出在 LLM 输出格式和我们的预期对不上,纯文本硬挤 JSON 就是不靠谱。咱们换个更正规的方式------直接用智谱 AI 原生的 tool calling 接口,让模型按结构化格式输出工具调用,这才是生产环境的正确做法。

用智谱原生 SDK 的 tool_calls,这是 OpenAI 格式的标准做法------模型直接输出结构化的工具调用指令,不是从文本里抠,所以不可能解析失败。

ReAct Agent翻车了

仔细看结果,有两个场景的判断有点意思:

M001 正常交易:判了"中"风险,但实际这是A类商户、白天500元小额借记卡,按常理应该是"低"才对。LLM把"夜间交易占比低"这种好因素也列进风险因素里了,逻辑有点混乱。 M999 黑名单商户:判了"中"风险,但它明明在黑名单里啊!而且5步推理全是"💬 思考",一次工具都没调用------LLM自己脑补了商户信息和交易数据,直接编了个结论。

这两个问题其实是ReAct Agent非常经典的坑: "工具调用意愿"不稳定:有些场景下模型会偷懒,觉得"我大概知道"就不调用了,直接靠幻觉编数据 判断逻辑不严谨:风险因素和风险等级的对应关系没有严格规则

为什么AI会"偷懒"和"胡说"

坑1:M001正常交易被判成"中"风险

这不是bug,这是LLM的概率本质 决定的。 你可以这么理解:LLM不是在"判断",它是在"猜下一个最可能出现的词"。当它读完商户信息(A级、白天、小额),它脑子里出现的可能是: 低风险:65%概率 中风险:30%概率 高风险:5%概率 它选了"中",不是因为它真的认为是中,而是因为它训练数据里类似语境下"中等风险"这个词出现的频率不低,再加上温度值设了0.1(不是0),有一定随机性。 更搞笑的是风险因素里列了"夜间交易占比低"------这明显是个好因素,但它写进了风险因素里。这说明LLM根本不理解"风险因素"这四个字的含义,它只是在模仿训练数据里看到的格式:"风险因素:...",然后往里面塞东西。 这就是为什么需要Prompt工程、需要Few-shot、需要结构化输出------你得用各种手段把LLM的随机性往你想要的方向收。

坑2:M999黑名单商户,一次工具都不调用

这个更关键------LLM的"工具调用意愿"是不稳定的 。 为什么M001、M002、M003都调用了工具,M999就不调用?因为LLM判断"我好像已经知道答案了",或者"这个问题不需要查"。它的判断依据是: 训练数据里类似的问题,哪些场景会调用工具 当前上下文的信息量够不够它编一个答案

M999的场景描述比较简短("商户M999发起一笔10000元交易"),LLM可能觉得"我直接分析也行",就懒得调用了。然后它开始自己编数据------什么"普通等级""电商行业""日均5万""夜间占比20%",全是它脑补的。 这在Agent领域有个专门的词叫"幻觉工具调用 "或者"跳过工具直接作答 "。生产环境里怎么解决?

  1. 强制调用:关键场景(比如黑名单检查)强制必须调用,不调用就不允许输出结论
  2. 多轮校验:用另一个LLM检查"这个结论是否有工具结果支撑"
  3. 规则兜底:如果5步里一次工具都没调,直接标记为"需人工复核"
相关推荐
smallyoung1 小时前
Spring AI 2.0 VectorStore实战:从原理到RAG落地
人工智能·后端
火山引擎开发者社区2 小时前
被 Vibe Coding 用户频点名的火山 Supabase 到底是个啥?一图来看懂
人工智能
火山引擎开发者社区2 小时前
动手做 AI 实验赢好礼!产品 + 大模型免费额度限时供应!
人工智能
字节跳动视频云技术团队2 小时前
从 VCloud 到 Agentic VCloud:Agent 时代的范式重构
人工智能·音视频开发
AKAMAI3 小时前
每百万 Token 成本砍六成,出海 AI 团队开始重算推理这笔账
人工智能·云计算
用户938515635074 小时前
从 Prompt 到 Harness:AI 工程化的三年跃迁与实战解码
javascript·人工智能
甲维斯4 小时前
Agnes免费生图批图API+一键生图软件!
人工智能
April6665 小时前
Prompt-only 已死,Harness 才是 2026 的分水岭
人工智能
没落英雄5 小时前
从零开始搭建一个 AI Agent —— LangChain + TypeScript 实战手记
前端·人工智能·架构