从零实现 Function Call:原理、架构与工程实践

从零实现 Function Call:原理、架构与工程实践

副标题 :以「混元 Lite 对话程序」为案例的深度技术解析

作者 :jlike

日期:2026-06-17


目录

  1. [引言:为什么大模型需要 Function Call](#引言:为什么大模型需要 Function Call)
  2. [Function Call 核心原理](#Function Call 核心原理)
    • [2.1 什么是 Function Call](#2.1 什么是 Function Call)
    • [2.2 谁在"调用"函数](#2.2 谁在"调用"函数)
    • [2.3 完整运行流程](#2.3 完整运行流程)
  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. 架构图与设计模式
    • [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. 进阶:边界、重试与优化
    • [5.1 边界情况全景梳理](#5.1 边界情况全景梳理)
    • [5.2 上下文回滚机制](#5.2 上下文回滚机制)
    • [5.3 错误重试策略](#5.3 错误重试策略)
    • [5.4 性能优化策略](#5.4 性能优化策略)
    • [5.5 生产环境 Checklist](#5.5 生产环境 Checklist)
  6. 总结与展望

1. 引言:为什么大模型需要 Function Call

大语言模型(LLM)本质上是概率文本生成器------给它一段上下文,它预测下一个 token。在纯文本对话中,这一能力足以应对大多数问答场景。但现实世界的需求远不止此:

  • "帮我查一下今天北京的气温"------模型不知道实时气温
  • "桌面上有多少个 PDF 文件?"------模型无法访问你的文件系统
  • "帮我在这个坐标附近找三家评分最高的餐厅"------模型没有地图数据库

Function Call 正是解决"模型能说但不能做"这一根本矛盾的核心机制。 它让 LLM 从一个"只会聊天的顾问"升级为"能调工具的行动者"。

本文将以一个真实的开源项目**混元 Lite 对话程序**为案例,从原理到代码、从架构到优化,完整拆解 Function Call 的实现全过程。本文涉及的所有代码均为项目中的实际代码,可直接运行验证。


2. Function Call 核心原理

2.1 什么是 Function Call

用一句话定义:

Function Call 是一种协议机制:客户端在每次请求中将"可用工具"以 JSON Schema 形式告知模型,模型在推理时自主决定是否需要调用某个工具。如果需要,它不执行任何代码,而是返回一个结构化的"调用请求"(函数名 + 参数),由客户端在本地真正执行,并将结果回传给模型生成最终回复。

关键在于理解两个"不"

  1. 模型不执行任何函数------它只输出"我想调用哪个函数、参数是什么"
  2. 模型不保存工具定义 ------每次请求都要重新传 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": "{}"
        }
      }]
    }
  }]
}

注意 contentnull------模型在这条消息里不输出文本,只输出一个"调用指令"。真正执行 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 要调用的函数必须满足两个条件:

  1. 返回字符串 ------因为回传给 API 的 tool 消息的 content 字段是 string 类型
  2. 无副作用依赖------函数在 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 参数定义 每个参数要有 typedescription
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,
}

扩展性设计:每新增一个工具,只需:

  1. 写好工具函数(返回 str
  2. TOOLS 中追加一条 Schema
  3. 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 系统。回顾关键要点:

  1. Function Call 的本质是"模型提建议,程序来执行"的分工协作,不是模型在远程调用
  2. JSON Schema 是模型理解工具的唯一边界,description 写得好坏直接决定触发准确率
  3. FC 是一个循环而非单次请求 ,需要在 API 调用层封装 请求→判断→执行→回传 的往返逻辑
  4. 三道防线 (JSON 解析 → 函数查找 → 异常捕获)+ 回滚快照构成了健壮的容错体系
  5. 分层设计 让 FC 对上层透明,新增工具只需在 TOOLSFUNCTION_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

许可:本文随项目源码一同开源,欢迎转载并注明出处。