你要学什么
前一步是"人告诉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
跑完后想一想
- 高风险交易和正常交易,Agent 的决策步数有差异吗?为什么?
- Agent 先查什么工具?为什么是这个顺序?这和真实风控人员的思路一致吗?
- Agent 比单次 LLM 调用好在哪?代价是什么?(Token 消耗、延迟)
- 什么场景适合用 Agent?什么场景用一次 LLM 调用就够了?
- 如果 Agent 在 Step 1 调错了工具,后续能自我修正吗?
Stage 3 和Stage 4,手动 Function Calling vs 原生 SDK ReAct Agent的差异
一、先分清两段代码对应的两种模式
- 上一段 LlamaIndex 手动循环版(人 / 代码控制工具逻辑) 你之前那段混合 RAG 的代码:function_calling_loop
- 当前纯智谱 SDK 原生 Tool Calling ReAct Agent(AI 自主规划行动) 现在这份无 LlamaIndex、原生zhipuai的代码
二、核心区别:谁来判断「要不要调用工具、调用哪个、调用几次」
- 旧手动版:代码强制给固定工具列表,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 拥有自主决策权
- 工具通过SDK标准tools参数传入,不是塞进prompt文本
ini
response = client.chat.completions.create(
model="glm-4-flash",
messages=messages,
tools=TOOLS_DEF, # SDK原生工具注册,模型原生识别工具能力
temperature=0.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领域有个专门的词叫"幻觉工具调用 "或者"跳过工具直接作答 "。生产环境里怎么解决?
- 强制调用:关键场景(比如黑名单检查)强制必须调用,不调用就不允许输出结论
- 多轮校验:用另一个LLM检查"这个结论是否有工具结果支撑"
- 规则兜底:如果5步里一次工具都没调,直接标记为"需人工复核"