从零实现 Function Call:原理、架构与工程实践
副标题 :以「混元 Lite 对话程序」为案例的深度技术解析
作者 :jlike
日期:2026-06-17
目录
- [引言:为什么大模型需要 Function Call](#引言:为什么大模型需要 Function Call)
- [Function Call 核心原理](#Function Call 核心原理)
- [2.1 什么是 Function Call](#2.1 什么是 Function Call)
- [2.2 谁在"调用"函数](#2.2 谁在"调用"函数)
- [2.3 完整运行流程](#2.3 完整运行流程)
- [实战:逐步实现 Function Call](#实战:逐步实现 Function Call)
- [3.1 步骤一:编写本地函数](#3.1 步骤一:编写本地函数)
- [3.2 步骤二:构建 JSON Schema 描述](#3.2 步骤二:构建 JSON Schema 描述)
- [3.3 步骤三:注册函数映射表](#3.3 步骤三:注册函数映射表)
- [3.4 步骤四:实现派发器](#3.4 步骤四:实现派发器)
- [3.5 步骤五:改造 API 调用层------封装 FC 往返循环](#3.5 步骤五:改造 API 调用层——封装 FC 往返循环)
- [3.6 步骤六:对话层保持透明](#3.6 步骤六:对话层保持透明)
- [3.7 完整运行效果](#3.7 完整运行效果)
- 架构图与设计模式
- [4.1 系统总体架构](#4.1 系统总体架构)
- [4.2 Function Call 数据流](#4.2 Function Call 数据流)
- [4.3 FC 往返循环时序图](#4.3 FC 往返循环时序图)
- [4.4 通用开发套路](#4.4 通用开发套路)
- [4.5 提炼:FC 设计模式](#4.5 提炼:FC 设计模式)
- 进阶:边界、重试与优化
- [5.1 边界情况全景梳理](#5.1 边界情况全景梳理)
- [5.2 上下文回滚机制](#5.2 上下文回滚机制)
- [5.3 错误重试策略](#5.3 错误重试策略)
- [5.4 性能优化策略](#5.4 性能优化策略)
- [5.5 生产环境 Checklist](#5.5 生产环境 Checklist)
- 总结与展望
1. 引言:为什么大模型需要 Function Call
大语言模型(LLM)本质上是概率文本生成器------给它一段上下文,它预测下一个 token。在纯文本对话中,这一能力足以应对大多数问答场景。但现实世界的需求远不止此:
- "帮我查一下今天北京的气温"------模型不知道实时气温
- "桌面上有多少个 PDF 文件?"------模型无法访问你的文件系统
- "帮我在这个坐标附近找三家评分最高的餐厅"------模型没有地图数据库
Function Call 正是解决"模型能说但不能做"这一根本矛盾的核心机制。 它让 LLM 从一个"只会聊天的顾问"升级为"能调工具的行动者"。
本文将以一个真实的开源项目**混元 Lite 对话程序**为案例,从原理到代码、从架构到优化,完整拆解 Function Call 的实现全过程。本文涉及的所有代码均为项目中的实际代码,可直接运行验证。
2. Function Call 核心原理
2.1 什么是 Function Call
用一句话定义:
Function Call 是一种协议机制:客户端在每次请求中将"可用工具"以 JSON Schema 形式告知模型,模型在推理时自主决定是否需要调用某个工具。如果需要,它不执行任何代码,而是返回一个结构化的"调用请求"(函数名 + 参数),由客户端在本地真正执行,并将结果回传给模型生成最终回复。
关键在于理解两个"不":
- 模型不执行任何函数------它只输出"我想调用哪个函数、参数是什么"
- 模型不保存工具定义 ------每次请求都要重新传
tools,不存在"注册到模型"的概念
2.2 谁在"调用"函数
这是一个常见的误解纠正:
❌ 错误理解:模型调用函数 → 远程服务器执行 → 返回结果
✅ 正确理解:模型建议调用函数 → 你的程序执行 → 把结果塞回对话 → 模型基于结果生成回复
用代码说话:模型返回的是这样的 JSON 结构:
json
{
"choices": [{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "count_pdf_on_desktop",
"arguments": "{}"
}
}]
}
}]
}
注意 content 是 null------模型在这条消息里不输出文本,只输出一个"调用指令"。真正执行 count_pdf_on_desktop() 并拿到文件列表的是你的 Python 程序。
2.3 完整运行流程
#mermaid-svg-x9VQRRwbxUtDgRJb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-x9VQRRwbxUtDgRJb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-x9VQRRwbxUtDgRJb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-x9VQRRwbxUtDgRJb .error-icon{fill:#552222;}#mermaid-svg-x9VQRRwbxUtDgRJb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-x9VQRRwbxUtDgRJb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-x9VQRRwbxUtDgRJb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-x9VQRRwbxUtDgRJb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-x9VQRRwbxUtDgRJb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-x9VQRRwbxUtDgRJb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-x9VQRRwbxUtDgRJb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-x9VQRRwbxUtDgRJb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-x9VQRRwbxUtDgRJb .marker.cross{stroke:#333333;}#mermaid-svg-x9VQRRwbxUtDgRJb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-x9VQRRwbxUtDgRJb p{margin:0;}#mermaid-svg-x9VQRRwbxUtDgRJb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-x9VQRRwbxUtDgRJb .cluster-label text{fill:#333;}#mermaid-svg-x9VQRRwbxUtDgRJb .cluster-label span{color:#333;}#mermaid-svg-x9VQRRwbxUtDgRJb .cluster-label span p{background-color:transparent;}#mermaid-svg-x9VQRRwbxUtDgRJb .label text,#mermaid-svg-x9VQRRwbxUtDgRJb span{fill:#333;color:#333;}#mermaid-svg-x9VQRRwbxUtDgRJb .node rect,#mermaid-svg-x9VQRRwbxUtDgRJb .node circle,#mermaid-svg-x9VQRRwbxUtDgRJb .node ellipse,#mermaid-svg-x9VQRRwbxUtDgRJb .node polygon,#mermaid-svg-x9VQRRwbxUtDgRJb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-x9VQRRwbxUtDgRJb .rough-node .label text,#mermaid-svg-x9VQRRwbxUtDgRJb .node .label text,#mermaid-svg-x9VQRRwbxUtDgRJb .image-shape .label,#mermaid-svg-x9VQRRwbxUtDgRJb .icon-shape .label{text-anchor:middle;}#mermaid-svg-x9VQRRwbxUtDgRJb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-x9VQRRwbxUtDgRJb .rough-node .label,#mermaid-svg-x9VQRRwbxUtDgRJb .node .label,#mermaid-svg-x9VQRRwbxUtDgRJb .image-shape .label,#mermaid-svg-x9VQRRwbxUtDgRJb .icon-shape .label{text-align:center;}#mermaid-svg-x9VQRRwbxUtDgRJb .node.clickable{cursor:pointer;}#mermaid-svg-x9VQRRwbxUtDgRJb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-x9VQRRwbxUtDgRJb .arrowheadPath{fill:#333333;}#mermaid-svg-x9VQRRwbxUtDgRJb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-x9VQRRwbxUtDgRJb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-x9VQRRwbxUtDgRJb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-x9VQRRwbxUtDgRJb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-x9VQRRwbxUtDgRJb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-x9VQRRwbxUtDgRJb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-x9VQRRwbxUtDgRJb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-x9VQRRwbxUtDgRJb .cluster text{fill:#333;}#mermaid-svg-x9VQRRwbxUtDgRJb .cluster span{color:#333;}#mermaid-svg-x9VQRRwbxUtDgRJb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-x9VQRRwbxUtDgRJb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-x9VQRRwbxUtDgRJb rect.text{fill:none;stroke-width:0;}#mermaid-svg-x9VQRRwbxUtDgRJb .icon-shape,#mermaid-svg-x9VQRRwbxUtDgRJb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-x9VQRRwbxUtDgRJb .icon-shape p,#mermaid-svg-x9VQRRwbxUtDgRJb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-x9VQRRwbxUtDgRJb .icon-shape .label rect,#mermaid-svg-x9VQRRwbxUtDgRJb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-x9VQRRwbxUtDgRJb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-x9VQRRwbxUtDgRJb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-x9VQRRwbxUtDgRJb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} tool_calls
stop
用户输入:桌面上有多少PDF?
① 构建请求体
messages + tools 定义
② POST 到混元 API
③ 模型判断
finish_reason = ?
④ 提取 tool_calls 数组
{name: 'count_pdf_on_desktop', args: {}}
⑤ 本地执行函数
count_pdf_on_desktop()
⑥ 返回结果字符串
'桌面上有 3 个 PDF...'
⑦ 将结果以 role='tool'
追加到 messages
⑧ 提取 content
生成自然语言回复
⑨ 展示给用户
这个流程图揭示了 Function Call 最核心的设计:它是一个循环(Loop),不是一次性的请求-响应。 模型可能在一次用户输入后触发多轮工具调用,直到它认为信息足够生成最终回复。
3. 实战:逐步实现 Function Call
本节以本项目为实例,以自底向上的顺序展示六个实现步骤。每一步都附带实际代码和关键注释。
项目背景 :混元 Lite 对话程序是一个 Python CLI 工具,原本只支持基础的多轮对话。我们新增的 FC 功能是:当用户询问"桌面上有多少 PDF 文件"时,模型自动调用
count_pdf_on_desktop()函数来获取真实的文件统计数据。
3.1 步骤一:编写本地函数
FC 要调用的函数必须满足两个条件:
- 返回字符串 ------因为回传给 API 的
tool消息的content字段是 string 类型 - 无副作用依赖------函数在 FC 上下文中执行,应尽量纯净
改造前(原始实现,直接 print,无法被 FC 使用):
python
# count_desktop_pdf.py(改造前)
def count_pdf_on_desktop():
desktop = os.path.join(os.environ['USERPROFILE'], 'Desktop')
pdf_files = glob.glob(os.path.join(desktop, '*.pdf'))
print(f"PDF 文件数量: {len(pdf_files)}") # ❌ 打印到 stdout,FC 无法获取
改造后(返回字符串,FC 可调用):
python
# count_desktop_pdf.py(改造后,第 10-37 行)
def count_pdf_on_desktop():
"""统计桌面上的 .pdf 文件数量,返回格式化的统计结果字符串"""
desktop = os.path.join(os.environ['USERPROFILE'], 'Desktop')
if not os.path.exists(desktop):
return f"桌面路径不存在: {desktop}"
# 匹配 .pdf 和 .PDF(glob 区分大小写,Windows 下需分别匹配)
pdf_files = glob.glob(os.path.join(desktop, '*.pdf'))
pdf_files += glob.glob(os.path.join(desktop, '*.PDF'))
pdf_files = list(set(pdf_files)) # 去重
lines = [
f"桌面路径: {desktop}",
f"PDF 文件数量: {len(pdf_files)}",
]
if pdf_files:
lines.append("")
lines.append("文件列表:")
for f in sorted(pdf_files):
lines.append(f" {os.path.basename(f)}")
return "\n".join(lines) # ✅ 返回字符串,FC 可直接使用
设计要点:返回值要结构化、信息密度高。模型拿到这段字符串后,需要从中提取有用信息来组织自然语言回复------返回内容越清晰,模型生成的回复质量越高。
3.2 步骤二:构建 JSON Schema 描述
JSON Schema 是模型理解"你能做什么"的唯一信息来源。写得好坏直接决定模型能否在正确时机触发函数调用。
核心字段说明:
| 字段 | 作用 | 易错点 |
|---|---|---|
name |
函数唯一标识 | 必须与 FUNCTION_MAP 的 key 严格一致 |
description |
模型判断何时调用的核心依据 | 要写清楚触发场景,不要太抽象 |
parameters.type |
固定为 "object" |
即使无参也不要用其他类型 |
parameters.properties |
参数定义 | 每个参数要有 type 和 description |
parameters.required |
必填参数列表 | 漏写会导致模型不传该参数 |
本项目中的完整定义 (chat_cli.py 第 45-58 行):
python
TOOLS = [
{
"type": "function",
"function": {
"name": "count_pdf_on_desktop",
"description": (
"统计当前用户 Windows 桌面上的 PDF 文件数量和文件名列表。"
"当用户想了解桌面有哪些 PDF 文件、需要整理桌面文档时使用。"
),
"parameters": {
"type": "object",
# 该函数无参数,所以 properties 为空对象
"properties": {},
"required": [],
},
},
},
]
Schema 编写技巧:
description是模型决策的"唯一入口",要像写产品需求一样写:什么场景触发?解决什么问题?- 参数的
description也同样重要------模型用它来理解"从用户输入中提取什么值填入这个参数" - 带有枚举值的参数用
"enum": ["a", "b", "c"]限定,减少模型传非法值的概率
3.3 步骤三:注册函数映射表
TOOLS 是给模型看的(JSON Schema),FUNCTION_MAP 是给程序用的(name → callable)。两者必须保持同步:
python
# chat_cli.py 第 62-64 行
from count_desktop_pdf import count_pdf_on_desktop # 导入实体函数
FUNCTION_MAP = {
"count_pdf_on_desktop": count_pdf_on_desktop,
}
扩展性设计:每新增一个工具,只需:
- 写好工具函数(返回
str) - 在
TOOLS中追加一条 Schema - 在
FUNCTION_MAP中添加 name → function 映射
三步即完成注册,无需修改任何其他代码。
3.4 步骤四:实现派发器
派发器(_execute_one_tool_call)负责解析模型返回的调用指令并执行对应的本地函数。它是整个 FC 链路中**"模型意图"到"机器行为"的翻译层**。
python
# chat_cli.py 第 105-138 行
def _execute_one_tool_call(tool_call):
"""执行单个工具调用,返回执行结果字符串"""
func_name = tool_call["function"]["name"]
func_args_str = tool_call["function"].get("arguments", "{}")
# ═══ 防线①:参数 JSON 解析失败 ═══
try:
func_args = json.loads(func_args_str)
except json.JSONDecodeError:
return f"[错误] 无法解析函数参数: {func_args_str}"
# ═══ 防线②:模型要求调用不存在的函数 ═══
func = FUNCTION_MAP.get(func_name)
if func is None:
return f"[错误] 未知函数: {func_name},可用函数: {list(FUNCTION_MAP.keys())}"
# ═══ 防线③:函数执行过程中抛异常 ═══
print(f"\n[工具调用] 执行函数: {func_name}()")
try:
result = func(**func_args)
print(f"[工具调用] 执行完成,返回 {len(result)} 个字符")
return result
except Exception as e:
error_msg = f"函数 {func_name} 执行异常: {e}"
print(f"[工具调用] {error_msg}")
return f"[错误] {error_msg}"
三道防线设计:
| 防线 | 场景 | 处理策略 |
|---|---|---|
json.JSONDecodeError |
模型生成的参数不是合法 JSON | 返回错误描述字符串 |
FUNCTION_MAP.get() 返回 None |
模型幻觉出一个不存在的函数名 | 返回可用函数列表 |
通用 Exception |
本地函数执行出错(如文件不存在) | 捕获异常,返回错误详情 |
关键设计决策 :三道防线都返回字符串而非 None 。因为最终这些结果要以 "role": "tool" 消息的形式追加到 messages 中,而 API 要求 tool 消息的 content 必须是有效字符串。返回 None 会导致后续请求出错。
3.5 步骤五:改造 API 调用层------封装 FC 往返循环
这是整个 FC 实现中最核心的部分。我们改造了 call_hunyuan() 函数,使其从"单次请求-响应"升级为"带工具调用循环的多轮请求"。
改造前的流程:
用户输入 → POST 请求 → 返回文本 → 展示
改造后的流程:
#mermaid-svg-XFT1FfRw5ES7MiJA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XFT1FfRw5ES7MiJA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XFT1FfRw5ES7MiJA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XFT1FfRw5ES7MiJA .error-icon{fill:#552222;}#mermaid-svg-XFT1FfRw5ES7MiJA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XFT1FfRw5ES7MiJA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XFT1FfRw5ES7MiJA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XFT1FfRw5ES7MiJA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XFT1FfRw5ES7MiJA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XFT1FfRw5ES7MiJA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XFT1FfRw5ES7MiJA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XFT1FfRw5ES7MiJA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XFT1FfRw5ES7MiJA .marker.cross{stroke:#333333;}#mermaid-svg-XFT1FfRw5ES7MiJA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XFT1FfRw5ES7MiJA p{margin:0;}#mermaid-svg-XFT1FfRw5ES7MiJA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XFT1FfRw5ES7MiJA .cluster-label text{fill:#333;}#mermaid-svg-XFT1FfRw5ES7MiJA .cluster-label span{color:#333;}#mermaid-svg-XFT1FfRw5ES7MiJA .cluster-label span p{background-color:transparent;}#mermaid-svg-XFT1FfRw5ES7MiJA .label text,#mermaid-svg-XFT1FfRw5ES7MiJA span{fill:#333;color:#333;}#mermaid-svg-XFT1FfRw5ES7MiJA .node rect,#mermaid-svg-XFT1FfRw5ES7MiJA .node circle,#mermaid-svg-XFT1FfRw5ES7MiJA .node ellipse,#mermaid-svg-XFT1FfRw5ES7MiJA .node polygon,#mermaid-svg-XFT1FfRw5ES7MiJA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XFT1FfRw5ES7MiJA .rough-node .label text,#mermaid-svg-XFT1FfRw5ES7MiJA .node .label text,#mermaid-svg-XFT1FfRw5ES7MiJA .image-shape .label,#mermaid-svg-XFT1FfRw5ES7MiJA .icon-shape .label{text-anchor:middle;}#mermaid-svg-XFT1FfRw5ES7MiJA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XFT1FfRw5ES7MiJA .rough-node .label,#mermaid-svg-XFT1FfRw5ES7MiJA .node .label,#mermaid-svg-XFT1FfRw5ES7MiJA .image-shape .label,#mermaid-svg-XFT1FfRw5ES7MiJA .icon-shape .label{text-align:center;}#mermaid-svg-XFT1FfRw5ES7MiJA .node.clickable{cursor:pointer;}#mermaid-svg-XFT1FfRw5ES7MiJA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XFT1FfRw5ES7MiJA .arrowheadPath{fill:#333333;}#mermaid-svg-XFT1FfRw5ES7MiJA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XFT1FfRw5ES7MiJA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XFT1FfRw5ES7MiJA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XFT1FfRw5ES7MiJA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XFT1FfRw5ES7MiJA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XFT1FfRw5ES7MiJA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XFT1FfRw5ES7MiJA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XFT1FfRw5ES7MiJA .cluster text{fill:#333;}#mermaid-svg-XFT1FfRw5ES7MiJA .cluster span{color:#333;}#mermaid-svg-XFT1FfRw5ES7MiJA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XFT1FfRw5ES7MiJA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XFT1FfRw5ES7MiJA rect.text{fill:none;stroke-width:0;}#mermaid-svg-XFT1FfRw5ES7MiJA .icon-shape,#mermaid-svg-XFT1FfRw5ES7MiJA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XFT1FfRw5ES7MiJA .icon-shape p,#mermaid-svg-XFT1FfRw5ES7MiJA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XFT1FfRw5ES7MiJA .icon-shape .label rect,#mermaid-svg-XFT1FfRw5ES7MiJA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XFT1FfRw5ES7MiJA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XFT1FfRw5ES7MiJA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XFT1FfRw5ES7MiJA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
非 200
200 OK
是
否
tool_calls
stop
是
否
超出 5 轮
call_hunyuan() 入口
original_length = len(messages)
← 快照,用于失败回滚
for round in range(5)
① POST 请求
payload 含 tools 定义
网络错误?
del messagesoriginal_length:
return None
HTTP 状态码?
③ 解析 JSON
解析失败?
④ finish_reason = ?
⑤ 处理工具调用
messages.append(assistant_msg)
逐个执行 _execute_one_tool_call()
messages.append(tool_result_msg)
content 非空?
return content
✅ 成功出口
关键代码剖析 (chat_cli.py 第 141-269 行):
python
def call_hunyuan(messages, api_key):
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
# ══════════════ 核心机制:回滚快照 ══════════════
# 记录进入函数时 messages 的长度。本轮对话产生的任何中间消息
#(assistant tool_calls 消息 + tool 结果消息)都将追加到此位置之后。
# 如果中途失败,通过 del messages[original_length:] 一键清理。
original_length = len(messages)
for _ in range(MAX_TOOL_ROUNDS): # MAX_TOOL_ROUNDS = 5
payload = build_payload(messages) # 每次请求都带 tools 定义
# ... HTTP 请求 & 错误处理(每个异常分支都有 del + return None)...
choice = data["choices"][0]
message = choice.get("message", {})
finish_reason = choice.get("finish_reason", "")
# ──── 核心分支 ────
if finish_reason == "tool_calls":
tool_calls = message.get("tool_calls", [])
if not tool_calls:
del messages[original_length:]
return None
# ① 将 assistant 的 tool_calls 消息保存到上下文
messages.append(message)
# ② 逐个执行工具调用
for tc in tool_calls:
result_text = _execute_one_tool_call(tc)
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"content": result_text,
})
# ③ continue → 回到循环顶部,把工具结果发给模型
continue
# ──── 正常文本回复 ────
content = message.get("content", "")
if not content:
del messages[original_length:]
return None
return content # ✅ 最终出口
# 超出最大轮次
del messages[original_length:]
return None
这段代码体现了三个重要设计:
设计①:回滚快照(original_length)
在 FC 循环中,messages 会被逐步追加中间消息。如果在第 2 轮网络出错,第 1 轮产生的 assistant(tool_calls) 和 tool(result) 消息就成了"脏数据"。通过 original_length 快照,失败时一行 del messages[original_length:] 即可回滚到进入函数时的干净状态。
设计②:每次请求都携带 tools
python
def build_payload(messages):
return {
"model": MODEL_NAME,
"messages": messages,
"stream": False,
"tools": TOOLS, # ← 每次请求都传
}
模型没有"记忆"工具定义------它每次推理都是 stateless 的,只看当前请求中的 tools 数组。所以无论第几轮请求,都必须传 tools。
设计③:消息角色必须严格遵守协议
FC 涉及四种消息角色,顺序和结构必须正确:
messages = [
{"role": "user", "content": "桌面上有多少PDF?"}, # 用户问题
{"role": "assistant", "content": None, # 模型的工具调用指令
"tool_calls": [{"id": "call_xxx", "function": {...}}]}, # (注意 content 为 null)
{"role": "tool", "tool_call_id": "call_xxx", # 工具执行结果
"content": "桌面路径: ...\nPDF 文件数量: 3\n..."},
{"role": "assistant", "content": "你桌面上有 3 个 PDF..."} # 最终自然语言回复
]
3.6 步骤六:对话层保持透明
FC 的复杂度被封装在 call_hunyuan() 内部,chat_loop() 完全不感知工具调用的存在:
python
# chat_cli.py 第 284-332 行(chat_loop 的 FC 相关改动仅此一行注释)
def chat_loop(api_key):
"""call_hunyuan() 内部自动处理 Function Call 往返
(工具调用对 chat_loop 透明)"""
messages = []
while True:
user_input = input("你: ").strip()
# ...退出检查、空输入过滤...
messages.append({"role": "user", "content": user_input})
reply = call_hunyuan(messages, api_key) # ← 一行调用,FC 往返全在内部
if reply is None:
messages.pop() # 失败回滚
continue
print(reply)
messages.append({"role": "assistant", "content": reply})
这就是分层设计的力量:对话层处理用户交互,FC 层处理工具编排,各司其职,互不干扰。
3.7 完整运行效果
启动程序后的实际交互:
=======================================================
混元 Lite AI 对话程序(支持 Function Call)
模型: hunyuan-lite
已注册函数: count_pdf_on_desktop
输入 exit / quit / q 退出对话
=======================================================
你: 我桌面上有多少个PDF文件?
[工具调用] 执行函数: count_pdf_on_desktop()
[工具调用] 执行完成,返回 156 个字符
AI: 你的桌面上共有 5 个 PDF 文件,分别是:
- 第一季度报告.pdf
- 合同模板.PDF
- 技术方案_v2.pdf
- meeting_notes.pdf
- 需求文档.pdf
你: quit
再见!
4. 架构图与设计模式
4.1 系统总体架构
#mermaid-svg-sDdsalH9mAaF64yx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sDdsalH9mAaF64yx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sDdsalH9mAaF64yx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sDdsalH9mAaF64yx .error-icon{fill:#552222;}#mermaid-svg-sDdsalH9mAaF64yx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sDdsalH9mAaF64yx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sDdsalH9mAaF64yx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sDdsalH9mAaF64yx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sDdsalH9mAaF64yx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sDdsalH9mAaF64yx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sDdsalH9mAaF64yx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sDdsalH9mAaF64yx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sDdsalH9mAaF64yx .marker.cross{stroke:#333333;}#mermaid-svg-sDdsalH9mAaF64yx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sDdsalH9mAaF64yx p{margin:0;}#mermaid-svg-sDdsalH9mAaF64yx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sDdsalH9mAaF64yx .cluster-label text{fill:#333;}#mermaid-svg-sDdsalH9mAaF64yx .cluster-label span{color:#333;}#mermaid-svg-sDdsalH9mAaF64yx .cluster-label span p{background-color:transparent;}#mermaid-svg-sDdsalH9mAaF64yx .label text,#mermaid-svg-sDdsalH9mAaF64yx span{fill:#333;color:#333;}#mermaid-svg-sDdsalH9mAaF64yx .node rect,#mermaid-svg-sDdsalH9mAaF64yx .node circle,#mermaid-svg-sDdsalH9mAaF64yx .node ellipse,#mermaid-svg-sDdsalH9mAaF64yx .node polygon,#mermaid-svg-sDdsalH9mAaF64yx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sDdsalH9mAaF64yx .rough-node .label text,#mermaid-svg-sDdsalH9mAaF64yx .node .label text,#mermaid-svg-sDdsalH9mAaF64yx .image-shape .label,#mermaid-svg-sDdsalH9mAaF64yx .icon-shape .label{text-anchor:middle;}#mermaid-svg-sDdsalH9mAaF64yx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sDdsalH9mAaF64yx .rough-node .label,#mermaid-svg-sDdsalH9mAaF64yx .node .label,#mermaid-svg-sDdsalH9mAaF64yx .image-shape .label,#mermaid-svg-sDdsalH9mAaF64yx .icon-shape .label{text-align:center;}#mermaid-svg-sDdsalH9mAaF64yx .node.clickable{cursor:pointer;}#mermaid-svg-sDdsalH9mAaF64yx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sDdsalH9mAaF64yx .arrowheadPath{fill:#333333;}#mermaid-svg-sDdsalH9mAaF64yx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sDdsalH9mAaF64yx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sDdsalH9mAaF64yx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sDdsalH9mAaF64yx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sDdsalH9mAaF64yx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sDdsalH9mAaF64yx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sDdsalH9mAaF64yx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sDdsalH9mAaF64yx .cluster text{fill:#333;}#mermaid-svg-sDdsalH9mAaF64yx .cluster span{color:#333;}#mermaid-svg-sDdsalH9mAaF64yx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sDdsalH9mAaF64yx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sDdsalH9mAaF64yx rect.text{fill:none;stroke-width:0;}#mermaid-svg-sDdsalH9mAaF64yx .icon-shape,#mermaid-svg-sDdsalH9mAaF64yx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sDdsalH9mAaF64yx .icon-shape p,#mermaid-svg-sDdsalH9mAaF64yx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sDdsalH9mAaF64yx .icon-shape .label rect,#mermaid-svg-sDdsalH9mAaF64yx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sDdsalH9mAaF64yx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sDdsalH9mAaF64yx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sDdsalH9mAaF64yx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 外部
函数模块
FC 编排层 call_hunyuan()
对话层 chat_loop()
用户层
工具执行层
用户输入
chat_loop 主循环
维护 messages\[\]
失败回滚
messages.pop()
FC 往返循环
for round in range(5)
original_length
回滚快照
build_payload()
每次请求注入 tools
_execute_one_tool_call()
派发器:三道防线
FUNCTION_MAP
name→callable
TOOLS\[\]
JSON Schema
count_desktop_pdf.py
count_pdf_on_desktop()
腾讯混元 API
hunyuan-lite
4.2 Function Call 数据流
#mermaid-svg-HOjcdFasJkhDYkbe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HOjcdFasJkhDYkbe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HOjcdFasJkhDYkbe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HOjcdFasJkhDYkbe .error-icon{fill:#552222;}#mermaid-svg-HOjcdFasJkhDYkbe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HOjcdFasJkhDYkbe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HOjcdFasJkhDYkbe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HOjcdFasJkhDYkbe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HOjcdFasJkhDYkbe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HOjcdFasJkhDYkbe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HOjcdFasJkhDYkbe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HOjcdFasJkhDYkbe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HOjcdFasJkhDYkbe .marker.cross{stroke:#333333;}#mermaid-svg-HOjcdFasJkhDYkbe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HOjcdFasJkhDYkbe p{margin:0;}#mermaid-svg-HOjcdFasJkhDYkbe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HOjcdFasJkhDYkbe .cluster-label text{fill:#333;}#mermaid-svg-HOjcdFasJkhDYkbe .cluster-label span{color:#333;}#mermaid-svg-HOjcdFasJkhDYkbe .cluster-label span p{background-color:transparent;}#mermaid-svg-HOjcdFasJkhDYkbe .label text,#mermaid-svg-HOjcdFasJkhDYkbe span{fill:#333;color:#333;}#mermaid-svg-HOjcdFasJkhDYkbe .node rect,#mermaid-svg-HOjcdFasJkhDYkbe .node circle,#mermaid-svg-HOjcdFasJkhDYkbe .node ellipse,#mermaid-svg-HOjcdFasJkhDYkbe .node polygon,#mermaid-svg-HOjcdFasJkhDYkbe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HOjcdFasJkhDYkbe .rough-node .label text,#mermaid-svg-HOjcdFasJkhDYkbe .node .label text,#mermaid-svg-HOjcdFasJkhDYkbe .image-shape .label,#mermaid-svg-HOjcdFasJkhDYkbe .icon-shape .label{text-anchor:middle;}#mermaid-svg-HOjcdFasJkhDYkbe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HOjcdFasJkhDYkbe .rough-node .label,#mermaid-svg-HOjcdFasJkhDYkbe .node .label,#mermaid-svg-HOjcdFasJkhDYkbe .image-shape .label,#mermaid-svg-HOjcdFasJkhDYkbe .icon-shape .label{text-align:center;}#mermaid-svg-HOjcdFasJkhDYkbe .node.clickable{cursor:pointer;}#mermaid-svg-HOjcdFasJkhDYkbe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HOjcdFasJkhDYkbe .arrowheadPath{fill:#333333;}#mermaid-svg-HOjcdFasJkhDYkbe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HOjcdFasJkhDYkbe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HOjcdFasJkhDYkbe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HOjcdFasJkhDYkbe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HOjcdFasJkhDYkbe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HOjcdFasJkhDYkbe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HOjcdFasJkhDYkbe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HOjcdFasJkhDYkbe .cluster text{fill:#333;}#mermaid-svg-HOjcdFasJkhDYkbe .cluster span{color:#333;}#mermaid-svg-HOjcdFasJkhDYkbe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HOjcdFasJkhDYkbe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HOjcdFasJkhDYkbe rect.text{fill:none;stroke-width:0;}#mermaid-svg-HOjcdFasJkhDYkbe .icon-shape,#mermaid-svg-HOjcdFasJkhDYkbe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HOjcdFasJkhDYkbe .icon-shape p,#mermaid-svg-HOjcdFasJkhDYkbe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HOjcdFasJkhDYkbe .icon-shape .label rect,#mermaid-svg-HOjcdFasJkhDYkbe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HOjcdFasJkhDYkbe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HOjcdFasJkhDYkbe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HOjcdFasJkhDYkbe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段五:回传
阶段四:执行
阶段三:决策
阶段二:请求
④ 用户输入
⑤ build_payload()
messages + tools
⑥ POST 请求
阶段一:注册
① 编写本地函数
返回 str
② 构建 JSON Schema
TOOLS\[\]
③ 注册映射
FUNCTION_MAP{}
⑦ 模型推理
⑧ tool_calls 响应
name + arguments
⑨ stop 响应
content 文本
⑩ _execute_one_tool_call()
⑪ 本地函数执行
⑫ tool 结果消息
⑬ 追加到 messages
⑭ 再次请求 API
⑮ 模型生成最终回复
4.3 FC 往返循环时序图
混元 API count_pdf_on_desktop() _execute_one_tool_call() call_hunyuan() chat_loop() 用户 混元 API count_pdf_on_desktop() _execute_one_tool_call() call_hunyuan() chat_loop() 用户 #mermaid-svg-mmFkyRFHi6an8oAv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mmFkyRFHi6an8oAv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mmFkyRFHi6an8oAv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mmFkyRFHi6an8oAv .error-icon{fill:#552222;}#mermaid-svg-mmFkyRFHi6an8oAv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mmFkyRFHi6an8oAv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mmFkyRFHi6an8oAv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mmFkyRFHi6an8oAv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mmFkyRFHi6an8oAv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mmFkyRFHi6an8oAv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mmFkyRFHi6an8oAv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mmFkyRFHi6an8oAv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mmFkyRFHi6an8oAv .marker.cross{stroke:#333333;}#mermaid-svg-mmFkyRFHi6an8oAv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mmFkyRFHi6an8oAv p{margin:0;}#mermaid-svg-mmFkyRFHi6an8oAv .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mmFkyRFHi6an8oAv text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-mmFkyRFHi6an8oAv .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mmFkyRFHi6an8oAv .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-mmFkyRFHi6an8oAv .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-mmFkyRFHi6an8oAv .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-mmFkyRFHi6an8oAv #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-mmFkyRFHi6an8oAv .sequenceNumber{fill:white;}#mermaid-svg-mmFkyRFHi6an8oAv #sequencenumber{fill:#333;}#mermaid-svg-mmFkyRFHi6an8oAv #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-mmFkyRFHi6an8oAv .messageText{fill:#333;stroke:none;}#mermaid-svg-mmFkyRFHi6an8oAv .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mmFkyRFHi6an8oAv .labelText,#mermaid-svg-mmFkyRFHi6an8oAv .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-mmFkyRFHi6an8oAv .loopText,#mermaid-svg-mmFkyRFHi6an8oAv .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-mmFkyRFHi6an8oAv .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mmFkyRFHi6an8oAv .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-mmFkyRFHi6an8oAv .noteText,#mermaid-svg-mmFkyRFHi6an8oAv .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-mmFkyRFHi6an8oAv .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mmFkyRFHi6an8oAv .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mmFkyRFHi6an8oAv .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mmFkyRFHi6an8oAv .actorPopupMenu{position:absolute;}#mermaid-svg-mmFkyRFHi6an8oAv .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-mmFkyRFHi6an8oAv .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mmFkyRFHi6an8oAv .actor-man circle,#mermaid-svg-mmFkyRFHi6an8oAv line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-mmFkyRFHi6an8oAv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} original_length = 2 "桌面上有多少PDF?" messages.append(user_msg) call_hunyuan(messages, key) POST messages + tools finish_reason="tool_calls" name="count_pdf_on_desktop" messages.append(assistant_tool_call_msg) _execute_one_tool_call(tc) count_pdf_on_desktop() "桌面路径: ...\nPDF数量: 3\n..." 结果字符串 messages.append(tool_result_msg) POST messages + tools(含工具结果) finish_reason="stop" "你桌面上有3个PDF文件..." "你桌面上有3个PDF文件..." messages.append(assistant_msg) "你桌面上有3个PDF文件..."
4.4 通用开发套路
从本项目的实战中,可以提炼出一套可复用的 FC 开发六步法:
┌──────────────────────────────────────────────────────────┐
│ Function Call 开发六步法 │
├──────────────────────────────────────────────────────────┤
│ │
│ ① 编写函数 │
│ 确保返回字符串,处理所有异常,不依赖全局状态 │
│ ↓ │
│ ② 构建 Schema │
│ description 要场景化,parameter 类型要精确 │
│ ↓ │
│ ③ 注册映射 │
│ TOOLS ↔ FUNCTION_MAP 双表同步 │
│ ↓ │
│ ④ 实现派发器 │
│ json.loads → FUNCTION_MAP.get → call → catch │
│ 三道防线,始终返回 str │
│ ↓ │
│ ⑤ 封装循环 │
│ for round in range(MAX): 请求→判断→执行→continue │
│ original_length 回滚快照 │
│ ↓ │
│ ⑥ 测试验证 │
│ 无工具调用场景、单工具调用、多工具调用、工具异常 │
│ │
└──────────────────────────────────────────────────────────┘
4.5 提炼:FC 设计模式
模式一:派发器模式(Dispatcher Pattern)
python
FUNCTION_MAP = {
"func_a": func_a,
"func_b": func_b,
"func_c": func_c,
}
def dispatch(tool_call):
name = tool_call["function"]["name"]
args = json.loads(tool_call["function"]["arguments"])
return FUNCTION_MAP[name](**args)
适用场景:需要支持多个异构工具调用时,用 map 替代 if-else 链。
优势:新增工具只需在 map 中加一行,零侵入。
模式二:回滚快照模式(Snapshot Rollback Pattern)
python
def call_with_rollback(messages):
snapshot = len(messages)
try:
# 可能产生中间消息的操作
do_something(messages)
except Exception:
del messages[snapshot:] # 一键回滚
raise
适用场景:操作过程中会修改共享状态(此处是 messages),但中途失败时需要恢复原状。
优势:不需要在每个失败点手动逐条删除。
模式三:透明代理模式(Transparent Proxy Pattern)
chat_loop() → 不感知 FC 的存在
│
call_hunyuan() → 内部封装所有 FC 逻辑
│
_excute_one_tool_call() → 只负责单个工具的执行
适用场景:在不改变上层接口的前提下,为底层增加新能力。
优势:对话层代码零改动即获得 FC 能力。
5. 进阶:边界、重试与优化
5.1 边界情况全景梳理
| # | 边界场景 | 风险 | 本项目的处理策略 |
|---|---|---|---|
| 1 | 模型返回 tool_calls 但数组为空 |
程序陷入逻辑死胡同 | if not tool_calls: 回滚 → return None |
| 2 | 模型生成不存在的函数名(幻觉) | 派发器 KeyError | FUNCTION_MAP.get() 返回 None → 返回错误描述字符串 |
| 3 | 模型生成的 arguments 不是合法 JSON | json.loads 抛异常 | try-except JSONDecodeError → 返回错误字符串 |
| 4 | 本地函数执行时抛异常(如文件被删除) | 未捕获异常导致程序崩溃 | 通用 except Exception 捕获 + 错误字符串 |
| 5 | FC 循环内某一次 HTTP 请求失败 | messages 中有脏中间消息 | original_length 快照回滚 |
| 6 | 模型反复调用工具不停止(循环依赖) | 死循环耗尽 API 额度 | MAX_TOOL_ROUNDS = 5 硬限制 |
| 7 | 工具返回的数据量过大 | 超出模型 token 上限 | 函数返回值应精简,避免全量数据 |
| 8 | 模型同时调用多个工具 | 结果顺序错乱 | 逐个执行,tool_call_id 一一对应 |
5.2 上下文回滚机制
这是本项目中最精妙的设计之一。让我们深入分析:
python
# chat_cli.py 第 163-164 行
original_length = len(messages) # ← 快照
# ... FC 循环(可能追加多条中间消息)...
# 任何失败出口:
del messages[original_length:] # ← 恢复到进入函数时的状态
为什么需要回滚?
假设 messages 进入 call_hunyuan 前有 4 条消息:
[0] user: "你好"
[1] assistant: "你好!"
[2] user: "帮我查一下天气"
[3] assistant: "好的,我需要调用 get_weather"
FC 第一轮中,模型触发 tool_calls,追加了 2 条:
[4] assistant(tool_calls): {调用 get_weather}
[5] tool(result): "北京今天 25°C"
此时网络断开,第二轮请求失败。如果不回滚,[4] 和 [5] 残留在 messages 中------下次对话时模型会看到一条不完整的工具调用链,导致行为异常。
回滚后:messages 回到 4 条,与进入函数前完全一致,下次对话从干净状态开始。
5.3 错误重试策略
本项目采用的是"不回退重试 + 整体返回失败"策略。适合 CLI 工具场景:失败了用户自己重新输入即可。
但对于生产级服务,建议分层设计重试:
python
# 推荐的 FC 重试分层策略(伪代码)
class FunctionCallExecutor:
MAX_FC_ROUNDS = 5 # FC 循环上限(硬限制,防止死循环)
MAX_RETRIES_PER_REQUEST = 3 # 单次 HTTP 请求重试(指数退避)
TOOL_EXEC_TIMEOUT = 30 # 工具执行超时(秒)
def execute(self, messages):
snapshot = len(messages)
for fc_round in range(self.MAX_FC_ROUNDS):
# ── 请求层重试(网络瞬断等可恢复错误)──
response = self._retry_request(messages)
if response is None:
del messages[snapshot:]
return None # 重试耗尽,整体失败
# ── 判断 FC 分支 ──
if response.finish_reason == "tool_calls":
for tc in response.tool_calls:
result = self._execute_with_timeout(tc)
messages.append({"role": "tool", ...})
continue # 继续 FC 循环
return response.content # 成功
del messages[snapshot:]
return None # FC 轮次耗尽
def _retry_request(self, messages):
"""带指数退避的请求重试"""
for attempt in range(self.MAX_RETRIES_PER_REQUEST):
try:
return requests.post(url, json=payload, timeout=...)
except (ConnectTimeout, ConnectionError):
if attempt == self.MAX_RETRIES_PER_REQUEST - 1:
return None
time.sleep(2 ** attempt) # 1s → 2s → 4s
重试设计原则:
| 错误类型 | 是否重试 | 原因 |
|---|---|---|
| 连接超时 / 连接重置 | ✅ 重试 | 网络瞬断,瞬时恢复概率高 |
| 读取超时 | ✅ 可重试 | 可能是服务端瞬时负载高 |
| HTTP 401 (鉴权失败) | ❌ 不重试 | 密钥错误,重试无意义 |
| HTTP 429 (速率限制) | ✅ 延迟重试 | 等冷却后重试 |
| HTTP 5xx (服务端错误) | ✅ 可重试 | 服务端可能瞬时故障 |
| 工具执行异常 | ❌ 不重试 FC | 将错误作为 tool 结果回传,让模型自行处理 |
5.4 性能优化策略
优化一:工具执行结果缓存
如果用户在同一个会话中两次问"桌面上有多少 PDF",没必要重复执行函数。可以引入简单缓存:
python
from functools import lru_cache
# 在工具函数上加缓存装饰器
@lru_cache(maxsize=32)
def count_pdf_on_desktop():
"""..."""
注意 :缓存要考虑 TTL------文件数量是会变化的。lru_cache 适合会话级缓存(进程存活期间),不适合跨会话持久化。
优化二:并行工具调用
当模型一次返回多个 tool_calls 时,如果它们之间无依赖关系,可以并行执行:
python
from concurrent.futures import ThreadPoolExecutor
def execute_tool_calls(tool_calls):
"""并行执行无依赖关系的工具调用"""
with ThreadPoolExecutor(max_workers=5) as pool:
futures = {
pool.submit(_execute_one_tool_call, tc): tc
for tc in tool_calls
}
results = []
for future in as_completed(futures):
results.append(future.result())
return results
前提:确保工具之间无数据依赖。本项目只有一个工具,不需要并行,但这是多工具场景的重要优化。
优化三:精确 token 计数替代消息条数裁剪
本项目使用 len(messages) > 40 作为裁剪阈值,这是简单但粗糙的策略。更精确的做法是基于 token 计数:
python
# 使用 tiktoken 做精确 token 计数
import tiktoken
def count_tokens(messages, model="gpt-3.5-turbo"):
encoding = tiktoken.encoding_for_model(model)
total = 0
for msg in messages:
total += len(encoding.encode(msg.get("content", "") or ""))
return total
# 当 token 数接近模型上限时裁剪
if count_tokens(messages) > MODEL_MAX_TOKENS * 0.8:
messages = trim_messages(messages)
适用场景:工具返回结果可能很长的场景(如返回了完整文件内容)。
5.5 生产环境 Checklist
将 FC 从 Demo 推向生产,需要检查以下清单:
| # | 检查项 | 本项目状态 |
|---|---|---|
| 1 | 所有工具函数返回 str |
✅ 已实现 |
| 2 | 工具函数有完整的异常捕获 | ✅ 三道防线 |
| 3 | 有 FC 循环上限 | ✅ MAX_TOOL_ROUNDS = 5 |
| 4 | 失败时可回滚中间状态 | ✅ original_length 快照 |
| 5 | Schema description 覆盖触发场景 | ✅ 场景化描述 |
| 6 | 工具执行有超时控制 | ⚠️ 当前未实现(建议添加) |
| 7 | 工具结果大小有限制 | ⚠️ 当前未限制(依赖函数自控) |
| 8 | 有请求重试机制 | ⚠️ 当前无重试(用户手动重试) |
| 9 | 有工具调用日志/审计 | ✅ [工具调用] 日志输出 |
| 10 | tool_call_id 严格对应 | ✅ 一一对应 |
6. 总结与展望
核心收获
通过本文的逐步拆解,我们从零实现了一个完整的 Function Call 系统。回顾关键要点:
- Function Call 的本质是"模型提建议,程序来执行"的分工协作,不是模型在远程调用
- JSON Schema 是模型理解工具的唯一边界,description 写得好坏直接决定触发准确率
- FC 是一个循环而非单次请求 ,需要在 API 调用层封装
请求→判断→执行→回传的往返逻辑 - 三道防线 (JSON 解析 → 函数查找 → 异常捕获)+ 回滚快照构成了健壮的容错体系
- 分层设计 让 FC 对上层透明,新增工具只需在
TOOLS和FUNCTION_MAP中各加一条
适用场景
Function Call 最适合以下场景:
- 数据查询:查数据库、查文件系统、调外部 API
- 操作执行:发送邮件、创建文件、修改配置
- 计算密集型:复杂数学运算、数据处理
- 多工具编排:先查天气 → 再查路线 → 最后推荐出行方案
局限与展望
当前实现是一个 CLI Demo,距离生产级 FC 系统还有这些可扩展方向:
- 流式响应中的 FC :在
stream=True模式下处理分片的tool_calls - 工具权限控制:不同用户/会话有不同的可用工具集
- 工具调用确认:高风险操作(删除文件等)需要用户二次确认
- Agent 循环:模型自主规划多步操作(ReAct / Plan-and-Execute 模式)
作者 :jlike
项目地址 :
fc-junyuan-lite技术栈 :Python 3.8+ / 腾讯混元 Lite / OpenAI Compatible API
许可:本文随项目源码一同开源,欢迎转载并注明出处。