Stage 3: Function Calling --- 让AI能动起来
你要学什么
前两步AI只能"读"知识库、"写"回答。但真实风控场景中,你需要查商户信息、查交易历史、冻结账户------这些是API调用,不是知识库检索。Function Calling就是让AI能"动手"的能力。
关键认知:Function Calling的本质是LLM输出一个结构化的"调用指令",你的代码解析后执行对应的函数,再把结果喂回LLM。 不是魔法,是协议。
运行代码
bash
cat > /Users/salar/ai-risk-agent/stage3_fc.py << 'PYEOF'
import os
import json
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
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")
api_key = os.environ.get("ZHIPUAI_API_KEY")
Settings.llm = ZhipuAI(model="glm-4-flash", temperature=0.1, api_key=api_key)
# ====== 1. 模拟支付API(真实的支付系统会有这些接口) ======
# 模拟商户数据
MERCHANTS = {
"M001": {
"name": "优品超市", "level": "A", "category": "零售",
"monthly_txn": 50000, "refund_rate": 0.02, "complaints": 0,
"blacklisted": False, "settle_cycle": "T+0"
},
"M002": {
"name": "环球数码", "level": "C", "category": "跨境电商",
"monthly_txn": 200000, "refund_rate": 0.18, "complaints": 15,
"blacklisted": False, "settle_cycle": "T+1"
},
"M003": {
"name": "速达贸易", "level": "D", "category": "贸易",
"monthly_txn": 800000, "refund_rate": 0.35, "complaints": 48,
"blacklisted": True, "settle_cycle": "已冻结"
},
}
# 模拟交易历史
TRANSACTIONS = {
"M001": [
{"id": "T001", "amount": 320, "time": "2026-06-22 10:15", "card": "debit", "status": "success"},
{"id": "T002", "amount": 1580, "time": "2026-06-22 11:30", "card": "credit", "status": "success"},
],
"M002": [
{"id": "T003", "amount": 28000, "time": "2026-06-22 03:20", "card": "credit", "status": "success"},
{"id": "T004", "amount": 27500, "time": "2026-06-22 03:25", "card": "credit", "status": "success"},
{"id": "T005", "amount": 29800, "time": "2026-06-22 03:30", "card": "credit", "status": "success"},
],
"M003": [
{"id": "T006", "amount": 4999, "time": "2026-06-22 02:10", "card": "credit", "status": "success"},
{"id": "T007", "amount": 4999, "time": "2026-06-22 02:12", "card": "credit", "status": "success"},
{"id": "T008", "amount": 4999, "time": "2026-06-22 02:14", "card": "credit", "status": "success"},
{"id": "T009", "amount": 4999, "time": "2026-06-22 02:16", "card": "credit", "status": "success"},
{"id": "T010", "amount": 4999, "time": "2026-06-22 02:18", "card": "credit", "status": "success"},
],
}
# 定义工具函数
def query_merchant(merchant_id: str) -> dict:
"""查询商户信息及风险等级"""
return MERCHANTS.get(merchant_id, {"error": "商户不存在"})
def query_transactions(merchant_id: str, hours: int = 24) -> list:
"""查询商户近期交易记录"""
return TRANSACTIONS.get(merchant_id, [])
def check_blacklist(merchant_id: str) -> dict:
"""检查商户是否在黑名单中"""
m = MERCHANTS.get(merchant_id)
if not m:
return {"error": "商户不存在"}
return {"blacklisted": m["blacklisted"], "reason": "高退款率+投诉集中" if m["blacklisted"] else ""}
def block_merchant(merchant_id: str, reason: str) -> dict:
"""冻结商户账户(高风险操作)"""
m = MERCHANTS.get(merchant_id)
if not m:
return {"error": "商户不存在"}
return {"action": "blocked", "merchant": merchant_id, "reason": reason, "timestamp": "2026-06-22T15:00:00"}
# 工具注册表
TOOLS = {
"query_merchant": {
"description": "查询商户信息及风险等级",
"parameters": {"merchant_id": "商户ID,如M001"},
"function": query_merchant,
},
"query_transactions": {
"description": "查询商户近期交易记录",
"parameters": {"merchant_id": "商户ID", "hours": "查询最近N小时,默认24"},
"function": query_transactions,
},
"check_blacklist": {
"description": "检查商户是否在黑名单中",
"parameters": {"merchant_id": "商户ID"},
"function": check_blacklist,
},
"block_merchant": {
"description": "冻结商户账户(需确认)",
"parameters": {"merchant_id": "商户ID", "reason": "冻结原因"},
"function": block_merchant,
},
}
# ====== 2. Function Calling实现(手动版) ======
def build_tool_prompt(transaction_info: str, context: str = "") -> str:
"""构建包含工具描述的Prompt"""
tool_desc = "\n".join([
f"- {name}: {t['description']}. 参数: {t['parameters']}"
for name, t in TOOLS.items()
])
return f"""你是一个支付风控助手。你需要评估以下交易的风险。
你可以使用以下工具来获取更多信息:
{tool_desc}
当你需要调用工具时,请输出JSON格式(不要输出其他内容):
{{"tool": "工具名", "parameters": {{"参数名": "参数值"}}}}
当你有足够信息做出判断时,请输出风险评估JSON:
{{"risk_level": "低/中/高/极高", "risk_factors": ["因素1", "因素2"], "action": "建议措施", "confidence": 0.0-1.0, "source": "引用的规则"}}
交易信息:{transaction_info}
{context}
请分析:"""
def call_tool(tool_name: str, params: dict) -> str:
"""执行工具调用"""
if tool_name not in TOOLS:
return json.dumps({"error": f"未知工具: {tool_name}"})
func = TOOLS[tool_name]["function"]
try:
result = func(**params)
return json.dumps(result, ensure_ascii=False)
except Exception as e:
return json.dumps({"error": str(e)})
def function_calling_loop(transaction_info: str, max_rounds: int = 3) -> dict:
"""Function Calling循环:LLM决定调什么 → 执行 → 喂回 → 再决定"""
llm = Settings.llm
context = ""
for round_num in range(max_rounds):
prompt = build_tool_prompt(transaction_info, context)
response = llm.complete(prompt)
output = str(response).strip()
# 去掉可能的markdown标记
if output.startswith("```"):
output = output.split("\n", 1)[1] if "\n" in output else output[3:]
output = output.rsplit("```", 1)[0]
try:
parsed = json.loads(output)
except json.JSONDecodeError:
# LLM没输出JSON,可能是直接给了结论
return {"risk_level": "无法判断", "raw_output": output}
# 判断是工具调用还是最终结论
if "tool" in parsed:
tool_name = parsed["tool"]
params = parsed.get("parameters", {})
print(f" 📞 Round {round_num+1}: 调用 {tool_name}({params})")
tool_result = call_tool(tool_name, params)
print(f" 📥 结果: {tool_result[:150]}")
# 把工具结果加入上下文,让LLM基于新信息继续判断
context += f"\n工具调用结果({tool_name}):{tool_result}"
else:
# LLM给了最终风险评估
print(f" 🎯 Round {round_num+1}: 给出风险评估")
return parsed
return {"risk_level": "无法判断", "reason": "超过最大轮次"}
# ====== 3. 构建RAG知识库 ======
print("正在构建知识库...")
doc_dir = "/Users/salar/ai-risk-agent/docs"
raw_docs = SimpleDirectoryReader(doc_dir).load_data()
splitter = SentenceSplitter(chunk_size=256, chunk_overlap=50)
nodes = splitter.get_nodes_from_documents(raw_docs)
index = VectorStoreIndex(nodes)
rag_engine = index.as_query_engine(similarity_top_k=3)
# ====== 4. 测试Function Calling ======
print("\n" + "="*60)
print("【Function Calling测试:AI自己决定调什么工具】")
print("="*60)
test_cases = [
{"desc": "正常交易", "info": "商户M001,借记卡交易500元,正常营业时间"},
{"desc": "可疑交易", "info": "商户M002,贷记卡交易48000元,凌晨3点"},
{"desc": "高风险交易", "info": "商户M003,连续5笔4999元,间隔2分钟"},
]
for case in test_cases:
print(f"\n{'─'*50}")
print(f"💳 场景: {case['desc']}")
print(f" 交易: {case['info']}")
print(f" 📊 AI决策过程:")
# 先用RAG查相关知识
rag_result = rag_engine.query(f"关于{case['desc']},有什么风控规则?")
enhanced_info = f"{case['info']}\n相关知识库内容:{rag_result}"
result = function_calling_loop(enhanced_info)
print(f"\n 📋 最终评估:")
for k, v in result.items():
print(f" - {k}: {v}")
print("\n" + "="*60)
print("💡 思考:")
print(" 1. AI在什么情况下选择调用工具?什么情况下直接给结论?")
print(" 2. 如果AI选错了工具,你会怎么优化Prompt?")
print(" 3. block_merchant这种高风险操作,应该让AI自动执行吗?(不该!需要人工确认)")
print(" 4. 这就是Function Calling的本质:LLM决定调什么,代码负责执行和校验")
PYEOF
bash
cd /Users/salar/ai-risk-agent && python stage3_fc.py
跑完后想一想
- AI先调了什么工具?为什么选那个?如果换一个顺序会怎样?
block_merchant这种冻结账户的操作,AI能直接调用吗?产品上应该怎么限制?- OpenAI的Function Calling和这个手动实现,本质区别是什么?(提示:没有本质区别,只是协议更标准化)
LLM是怎么判断要不要调用工具的
LLM每次都自己决定------它觉得信息不够就调工具,觉得够了就直接给结论。你的代码只负责"解析它的选择"。
具体看代码里 function_calling_loop 这个函数,核心就两步:
- 第一步:构建Prompt时,同时告诉LLM两件事:
json
你可以使用这些工具:query_merchant / query_transactions / check_blacklist / block_merchant
当你需要调工具时,输出:{"tool": "工具名", "parameters": {"参数": "值"}}
当你有足够信息时,输出:{"risk_level": "高/中/低", "action": "建议"}
关键:这两种输出格式都写在同一个Prompt里。LLM拿到交易信息后,自己判断当前信息够不够做风控评估
- 第二步:循环里解析LLM的输出:
bash
parsed = json.loads(output)
if "tool" in parsed:
# LLM说"我要查东西" → 执行工具 → 把结果塞回上下文 → 下一轮
elif ...:
# LLM说"我有结论了" → 直接返回风险评估
判断逻辑特别简单------输出的JSON里有没有 "tool" 这个key。有就是调工具,没有就是给结论。 没有魔法,就是一个约定好格式的循环。 LLM像个新员工------它不知道答案,但知道可以问谁。它选择问什么、问几次、什么时候可以下结论,都是它自己基于当前信息决定的。 这也是为什么Stage 3叫"手动版Function Calling"------真正的OpenAI API只是把这个格式约定标准化了(从JSON文本变成了结构化参数),但底层逻辑完全一样。
LLM是怎么判定信息足够了还是没有足够去做风控评估,以什么标准呢
这才是Function Calling真正微妙的地方。 直接说结论:LLM没有一个硬性的逻辑标准。它用的是"概率直觉"------基于Prompt里描述的评估框架,自己猜信息够不够。 具体来说,LLM做这个判断时依赖三层东西:
第一层:Prompt里定义的"评估框架" 你给LLM的最终输出格式是:
bash
risk_level / risk_factors / action / confidence / source
LLM会反推:要填这些字段,我需要哪些信息? risk_factors → 我得知道商户风险等级、交易模式、有没有在黑名单 source → 我得引用具体规则 所以它"觉得"应该先查商户信息、查黑名单、查交易历史。 但这只是"觉得",不是"必须"。 如果交易信息本身已经很明显了(比如"M003连续5笔4999元"太像拆分交易),LLM可能会跳过工具直接给结论------因为它"觉得"不需要查也够判断了。
第二层:工具描述里暗示的"应该查什么" Prompt里工具列表的描述会暗示LLM:
diff
- query_merchant: 查询商户信息及风险等级
- check_blacklist: 检查商户是否在黑名单中
- query_transactions: 查询交易记录
LLM看到这些描述,会想:"风控评估应该需要风险等级和黑名单信息,那我先查一下"。 如果你把工具描述改成"查询商户天气偏好",LLM大概率不会去调它------因为它不觉得这和风控有关。
第三层:LLM的训练模式
LLM在训练时见过大量"先收集信息→再做判断"的对话模式。当你给它一个任务+一堆工具时,它的"直觉"是:先查再说,不要瞎猜。 但这也是为什么它有时会"偷懒"------信息看起来太简单时,它可能觉得"这不需要查工具了"。 所以问题来了:这个判断其实很不可靠,对吧? 对。这就是Stage 3的局限性,也是为什么要有Stage 4。 Stage 3的LLM靠"直觉"决定调不调工具,有时候会漏查、有时候会多查。在实际生产环境,这很危险------风控不能靠LLM"觉得够了"。 Stage 4的ReAct模式就是为了解决这个问题------它强制LLM每一步都显式地写出来:
makefile
Think: 我已知交易是拆分交易,但不知道商户是否在黑名单,需要查。
Act: check_blacklist(M003)
Observe: 商户已在黑名单,reason=高退款率
Think: 现在信息够了,可以评估了。
每一步的Think是"写出来的思考",不再是黑箱直觉。你在代码里可以看到这个区别------Stage 4强制要求LLM先输出Thought:再决定Action:。
没有一个明确的标准。它靠三样东西综合判断:Prompt里的评估框架 → 工具描述的暗示 → 训练出来的"先查再说"直觉。这个判断是概率性的,不可靠的。
这也是为什么AI PM在设计Function Calling时,Prompt里要尽可能明确地写"你需要先确认商户风险等级和黑名单状态,然后才能做评估"------把"该查什么"写死在Prompt里,而不是让LLM自己猜。 跑完Stage 3你观察一下输出,看看LLM在每个场景分别调了几次工具、有没有跳过的,就能直观感受到这个"不可靠性"了