1. 项目缘起:为什么你的AI风控需要“动手能力”
在风控这个行当里干了十几年,我见过太多“纸上谈兵”的智能系统。它们能分析、能预警、能生成一份漂亮的报告,但一到关键时刻,需要它“动”起来——比如自动拦截一笔可疑交易、临时调整一个用户的信用额度、或者给运营同事发一条待处理的工单——就哑火了。最终,还是得靠人工去后台点点点,效率瓶颈和响应延迟就是这么来的。
我们之前聊过如何用大语言模型(LLM)去理解交易文本、分析用户行为,构建一个聪明的“大脑”。但一个只有大脑、没有手脚的助手,终究只是个参谋,成不了将军。Function Calling,就是给这个聪明的“大脑”装上可操控的“手脚”。它让LLM不仅能“想”,还能“做”——通过调用我们预先定义好的工具函数,去执行具体的业务操作。
举个例子,你的风控模型判断某笔深夜的高额境外交易风险极高。没有Function Calling,流程可能是:模型输出风险结论 -> 风控员看到告警 -> 登录后台系统 -> 手动查找该订单 -> 执行拦截操作。整个过程可能耗时几分钟,骗子早就完成洗钱跑路了。有了Function Calling,流程就变成了:模型判断风险 -> 自动调用“交易拦截”函数 -> 系统在毫秒级完成拦截。这中间的效率差和风险控制能力,是天壤之别。
所以,这个Stage 3,我们要解决的核心问题就是:如何安全、准确、高效地教会AI使用我们给它准备好的“工具”,让智能决策能瞬间转化为实际行动。这不是简单的API调用,而是一套关于意图理解、权限管控、执行反馈的完整工程体系。
2. Function Calling的本质:不是调用,是“对齐”与“调度”
很多人一听到Function Calling,第一反应就是“让AI去调我的代码函数”。这个理解对了一半,但更关键的一半被忽略了。LLM本身并不能直接执行你的Python或Java函数,它运行在远端的云端,没有你本地环境的权限。实际上,Function Calling是一个精巧的“对齐”过程。
它的工作流程更像一个经验丰富的指挥官和一支特种部队:
- 指挥官(LLM)分析战局(用户输入):它根据当前的对话上下文和用户的问题,判断是否需要动用“工具”,以及动用哪个“工具”最合适。
- 指挥官下达指令(生成结构化调用请求):它不会直接说“去调
block_transaction函数”,而是会按照我们事先约定好的格式,生成一个标准的、结构化的调用请求,比如{“name”: “blockTransaction”, “arguments”: {“order_id”: “123456”, “reason”: “高风险境外交易”}}。这个格式,就是我们定义的“工具描述”。 - 通信兵(你的应用代码)传递指令:你的后端服务收到这个结构化请求。
- 特种部队(你的本地函数)执行任务:你的代码解析这个请求,找到对应的本地函数
block_transaction(order_id, reason),传入参数,并执行它。这个函数拥有访问数据库、调用内部API、发送消息等所有必要的权限。 - 部队回传战报(执行结果):函数执行完成后,将结果(成功或失败,附带详情)再次封装成结构化的文本或数据。
- 指挥官整合战报,继续指挥:这个结果被送回到LLM,LLM结合这个“战报”和之前的对话历史,生成最终面向用户的自然语言回复,比如“已成功拦截订单123456,已记录原因为‘高风险境外交易’。”
所以,Function Calling的核心,是让LLM的理解与我们后端的执行能力“对齐”。我们通过“工具描述”告诉LLM我们有哪些能力、每个能力需要什么信息;LLM则负责在复杂的自然语言中,精准地识别出需要使用哪个能力,并提取出所需的参数。这是一个从非结构化(自然语言)到结构化(函数调用),再回到非结构化(自然语言回复)的闭环。
注意:安全是这里的生命线。LLM只是“建议”调用某个函数,最终是否执行、如何执行、参数是否合法,完全由你的后端代码掌控和校验。绝对不能让LLM的建议直接等同于执行。
3. 实战第一步:设计你的风控“工具包”
在写一行代码之前,我们必须像设计一套手术器械一样,精心设计我们的工具。工具不是越多越好,而是越精准、越安全越好。根据常见的风控场景,我通常会规划这么几个核心工具:
3.1 工具清单设计与思考
交易操作类
blockTransaction: 拦截/挂起一笔交易。这是最核心的“急刹车”功能。adjustUserCredit: 调整用户信用额度或设置交易限额。用于动态风险控制。flagUserForReview: 给用户打上风险标签,触发人工审核流程。
信息查询类
getTransactionDetails: 根据订单号查询交易详情(金额、时间、商户、IP等)。LLM在做决策前,可能需要更详细的数据。getUserBehaviorHistory: 获取用户近期登录、交易、设备变更等行为序列。用于上下文风险分析。queryRiskRules: 查询当前生效的风险规则及其命中情况。让AI了解“系统为什么这么判断”。
通知与协同类
createRiskCase: 在风控工单系统中创建一个案件,并分配给人审。sendAlertNotification: 向风控值班人员的钉钉/飞书群发送一条强提醒消息。
设计原则:
- 单一职责:一个工具只做一件事。
blockTransaction就只负责拦截,不要在里面又查用户信息又发通知。 - 参数明确:每个参数定义清晰的数据类型(string, number, boolean)和格式(如
order_id必须是32位字符串)。这能极大减少LLM的误解。 - 安全边界:在工具描述中,可以加入使用说明和警告,例如:“此工具将直接中断交易流程,请仅在确认高风险时使用。”
3.2 如何编写“工具描述”(以OpenAI格式为例)
“工具描述”是你和LLM之间的契约。一份好的描述,能极大提升调用的准确率。它本质上是一个JSON Schema。
{ "type": "function", "function": { "name": "blockTransaction", "description": "拦截一笔指定的支付交易。该操作将立即阻止交易完成,并将资金退回到用户账户或原支付渠道。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "需要拦截的支付订单的唯一标识符。" }, "reason": { "type": "string", "description": "拦截的原因代码或简短描述。例如:'suspected_fraud'(涉嫌欺诈)、'policy_violation'(违反策略)、'high_risk_region'(高风险地区)。", "enum": ["suspected_fraud", "policy_violation", "high_risk_region", "user_request", "system_error"] }, "comment": { "type": "string", "description": "给运营人员查看的补充说明或备注。" } }, "required": ["order_id", "reason"] } } }关键点解析:
description: 这是最重要的部分!要用清晰、无歧义的语言告诉LLM这个工具是干什么的、什么时候用。好的描述能直接让LLM做出正确判断。parameters: 定义每个参数。使用enum枚举来限制reason的取值,这是保证输入合规、便于后续统计的关键。required字段指明了哪些参数是LLM必须提供的。- 不要暴露内部细节:描述里不需要写“调用
/api/v1/risk/block这个POST接口”,那是你后端实现的事。LLM只需要知道业务逻辑。
4. 构建执行引擎:连接LLM与业务系统的桥梁
有了工具描述,接下来就要搭建一个可靠的“执行引擎”。这个引擎负责接收LLM的调用请求,安全地执行本地函数,并管理整个对话状态。
4.1 基础架构与流程
一个典型的执行引擎包含以下组件:
- 对话/状态管理器:维护与LLM的对话历史,管理多轮对话中工具调用的上下文。
- 工具注册中心:一个字典或配置,将工具名(
blockTransaction)映射到实际的可调用函数和它的描述。 - 参数解析与验证器:对LLM生成的参数进行二次校验(类型、范围、业务逻辑),这是防止“胡说八道”导致错误操作的关键防线。
- 函数执行器:在安全的沙盒或权限上下文中调用实际业务函数。
- 结果格式化器:将函数执行结果(可能是对象、异常等)转换成LLM能理解的自然语言或结构化文本。
4.2 核心代码实现(Python示例)
我们以一个简化但完整的过程来展示。假设我们使用OpenAI的ChatCompletion API。
import json import openai from typing import Dict, Any, Callable, List # 1. 工具注册中心 class FunctionRegistry: def __init__(self): self._functions: Dict[str, Callable] = {} self._descriptions: List[Dict] = [] def register(self, func: Callable, description: Dict): """注册一个工具函数及其描述""" self._functions[description["function"]["name"]] = func self._descriptions.append(description) return func # 方便用作装饰器 def get_function(self, name: str) -> Callable: return self._functions.get(name) def get_descriptions(self) -> List[Dict]: return self._descriptions # 2. 定义我们的风控工具函数 registry = FunctionRegistry() @registry.register def block_transaction(order_id: str, reason: str, comment: str = "") -> Dict[str, Any]: """实际拦截交易的业务函数""" # 这里是你的真实业务逻辑,例如: # 1. 校验订单状态是否可拦截 # 2. 调用支付系统的拦截接口 # 3. 记录风控操作日志 # 4. 可能触发异步通知 print(f"[业务执行] 拦截订单 {order_id}, 原因: {reason}, 备注: {comment}") # 模拟一个成功响应 return { "success": True, "order_id": order_id, "action": "blocked", "message": f"订单已成功拦截。退款流程已启动。" } # 关联上一步我们定义的工具描述 # 假设 tool_description_block 就是前面那个JSON对象 registry._descriptions.append(tool_description_block) # 3. 对话与执行引擎 class RiskControlAssistant: def __init__(self, registry: FunctionRegistry, client): self.registry = registry self.client = client # OpenAI客户端 self.messages = [] # 对话历史 def chat(self, user_input: str) -> str: """处理用户输入,可能涉及多轮工具调用""" self.messages.append({"role": "user", "content": user_input}) while True: # 调用LLM,传入工具描述 response = self.client.chat.completions.create( model="gpt-4", # 或 gpt-3.5-turbo messages=self.messages, tools=self.registry.get_descriptions(), # 关键:告诉LLM可用的工具 tool_choice="auto", # 让LLM自行决定是否调用工具 ) message = response.choices[0].message self.messages.append(message) # 将LLM的回复加入历史 # 检查LLM是否想要调用工具 if not message.tool_calls: # 没有工具调用,直接返回最终回复 return message.content # 处理一个或多个工具调用 for tool_call in message.tool_calls: function_name = tool_call.function.name function_args = json.loads(tool_call.function.arguments) # 从注册中心获取实际函数 function_to_call = self.registry.get_function(function_name) if not function_to_call: # 处理工具未找到的情况 result = {"error": f"Function {function_name} not found."} else: # !!!关键:在执行前进行业务参数校验!!! if not self._validate_arguments(function_name, function_args): result = {"error": "Invalid arguments provided."} else: # 安全地执行函数 try: result = function_to_call(**function_args) except Exception as e: result = {"error": f"Execution failed: {str(e)}"} # 将工具执行结果作为新的消息,反馈给LLM self.messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result), "name": function_name }) # 循环继续,LLM会根据工具执行结果生成下一轮回复 def _validate_arguments(self, func_name: str, args: Dict) -> bool: """加强版参数校验(示例)""" if func_name == "blockTransaction": # 检查order_id格式(假设是32位字母数字) order_id = args.get("order_id", "") if len(order_id) != 32 or not order_id.isalnum(): return False # 检查reason是否在允许的枚举值内 if args.get("reason") not in ["suspected_fraud", "policy_violation", "high_risk_region", "user_request", "system_error"]: return False return True # 4. 使用示例 if __name__ == "__main__": client = openai.OpenAI(api_key="your-api-key") assistant = RiskControlAssistant(registry, client) # 模拟一个风控指令 user_query = “发现订单ID ‘abc123xyz789def456ghi159jkl753mno852’ 存在欺诈特征,请立即拦截它,原因为涉嫌欺诈。” final_response = assistant.chat(user_query) print("助手回复:", final_response)这段代码的实战要点:
- 分离关注点:
FunctionRegistry负责管理工具,RiskControlAssistant负责对话流程。结构清晰,便于扩展。 - 强制校验:
_validate_arguments函数是必须的。LLM可能生成格式正确但业务逻辑错误的参数(比如一个不存在的order_id)。你的业务代码必须在执行前进行最终校验。 - 循环处理:
while True循环处理了多轮工具调用的可能。LLM可能先调用getTransactionDetails查询详情,再根据结果调用blockTransaction。 - 错误处理:工具执行可能失败(网络、数据库、业务规则冲突),必须捕获异常并将结构化的错误信息返回给LLM,让它能向用户解释。
5. 高级策略与避坑指南:让“智能”真正“可靠”
基础功能跑通只是第一步,要让这个系统在生产环境可靠运行,还需要一系列高级策略和防错机制。
5.1 权限管控与操作确认
绝对不能允许一个普通的客服AI助手拥有拦截所有交易的权限。必须引入权限层级和确认机制。
- 工具权限组:将工具分类,如
信息查询类、轻度操作类(如打标签)、重度操作类(如拦截、调额)。为不同的AI助手角色(如“风控分析员”、“值班机器人”、“客服助手”)绑定不同的权限组。 - 关键操作二次确认:对于
blockTransaction这类高风险操作,可以在执行引擎中增加一个确认环节。当LLM请求调用时,引擎先不执行,而是生成一条确认消息(“即将拦截订单XXX,原因YYY,请确认?”),等待人工或另一个高权限系统的确认后,再真正执行。这可以通过在对话流中插入一个特殊的“确认”工具来实现。
5.2 处理模糊与冲突的指令
用户输入是模糊的,LLM的理解也可能出现偏差。
- 场景:用户说“查一下张三最近的那笔大额交易”。LLM可能需要调用
getUserBehaviorHistory,但发现用户“张三”有多个,且“大额”定义不清。 - 策略:这时,LLM应该生成一个“澄清”性问题,而不是盲目猜测。这需要我们在系统设计时,鼓励LLM在参数不足时主动提问。我们可以通过在
system提示词中强调:“如果你无法从对话中确定执行某个工具所需的全部必要参数,请主动向用户提问以澄清。”
5.3 工具描述的“咒语”工程
工具描述的写法直接影响LLM的调用准确性,这本身就是一种“提示词工程”。
- 坏描述:
“处理订单。”(太模糊,LLM不知道什么时候用、怎么用) - 好描述:
“当一笔支付订单被判定为高风险或涉嫌欺诈时,使用此工具来立即阻止交易完成,并将资金置于冻结或退回状态。需要提供明确的订单标识和符合规定的风险原因代码。” - 技巧:在描述中使用“当...时”、“用于...场景”、“需要提供...”等句式,明确工具的触发条件和输入要求。甚至可以加入反面例子:“不要用于因用户普通争议而发起的退款。”
5.4 链路追踪与可解释性
所有通过Function Calling执行的操作,都必须留下完整的审计日志。
- 日志内容:原始用户请求、LLM生成的工具调用请求(含参数)、参数校验结果、实际执行函数的输入输出、最终返回给用户的答复。
- 价值:当出现误操作时,你可以完整回溯是用户的指令模糊,还是LLM理解错误,或是工具描述不当,亦或是业务函数本身的bug。这是后续迭代优化最重要的数据来源。
5.5 性能与成本考量
- 工具列表长度:每次请求都将所有工具描述发给LLM,会消耗Token。如果工具很多(比如超过50个),可以考虑动态加载:根据对话上下文,只加载可能相关的工具子集。
- 超时与重试:工具执行(尤其是调用外部API)可能超时。执行引擎需要设置合理的超时时间,并设计重试或降级策略(例如,拦截失败后,转为创建加急工单)。
- 限流与熔断:防止针对AI助手的恶意提问导致工具被高频调用,对业务系统造成压力。
6. 从Demo到生产:必须完成的清单
把实验代码变成生产系统,还有最后几公里要走,这几公里往往坑最多。
- 全面的错误处理与降级:网络抖动、LLM服务不可用、工具函数异常、数据库连接失败...每一个环节都要有对应的错误处理逻辑和友好的用户提示。至少要有“降级模式”:当Function Calling链路整体不可用时,AI助手能优雅地退化为一个只提供分析建议的“顾问”,而不是直接崩溃或给出误导信息。
- 输入清洗与防护:用户输入可能包含SQL注入、命令注入等恶意内容。虽然LLM本身有一定防护,但在将用户输入拼接进提示词,或作为参数传递给工具函数前,必须进行严格的清洗和转义。
- 版本化管理:工具函数和其描述会迭代更新。当你要修改一个工具的参数或行为时,如何保证不影响正在进行的对话?需要考虑工具描述的版本号,以及向后兼容的策略。
- 集成测试:必须建立一套完整的集成测试用例,模拟各种边界情况:模糊指令、参数缺失、参数错误、工具执行失败、多轮复杂交互等。这是保证系统稳定性的基石。
- 监控与告警:监控关键指标:工具调用成功率、各工具调用频次、平均响应时间、LLM调用消耗的Token数。对调用失败、高频调用异常工具等情况设置告警。
走到这里,你的AI风控助手才真正具备了“动手”的能力。它不再只是一个被动的分析器,而是一个能主动介入风险处理流程的智能体。你会发现,整个风控的响应速度和处理范式都会因此改变。当然,能力越大,责任越大。赋予AI行动力,意味着你需要更严谨的工程设计、更严密的安全管控和更细致的运维观察。但这一切的投入,在拦截住那笔即将造成重大损失的交易时,都会显得无比值得。