ChatGLM3工具调用实战:用Python函数扩展AI能力的完整教程
1. 为什么你需要掌握工具调用能力
你有没有遇到过这样的场景:
- 想让AI帮你查实时天气,它却只能告诉你“我无法访问网络”
- 需要计算一个复杂公式,它给出的只是文字描述而非精确结果
- 希望AI分析本地Excel数据,它却说“我看不到你的文件”
这些问题的本质,是大模型的“能力边界”——它再聪明,也只是个语言模型,没有真实世界的感知和执行能力。而工具调用(Function Calling),正是打破这堵墙的关键技术。
ChatGLM3-6B 是目前开源社区中对工具调用支持最成熟、文档最清晰、本地部署最友好的中文大模型之一。它不像某些模型需要复杂配置或云端服务,而是通过简洁的Python函数注册机制,让你在几行代码内就能赋予AI调用任意本地功能的能力。
本教程不讲抽象概念,不堆砌术语,只聚焦一件事:手把手带你把ChatGLM3变成一个真正能干活的智能助手。从零开始,完成环境准备、函数注册、对话触发、结果解析的全流程,最后你会拥有一个能查天气、算数学、读文件、甚至控制硬件的本地AI系统。
整个过程不需要GPU服务器,一台搭载RTX 3060及以上显卡的普通电脑即可流畅运行;如果你只有CPU,我们也会提供内存优化方案。现在,让我们开始这场实用主义的技术实践。
2. 环境准备与模型加载
2.1 基础依赖安装
打开终端,创建一个干净的Python环境(推荐使用conda):
conda create -n chatglm3-tool python=3.10 conda activate chatglm3-tool pip install torch==2.1.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install transformers==4.40.2 accelerate sentencepiece astunparse关键提示:必须锁定
transformers==4.40.2版本。这是ChatGLM3官方验证过的黄金版本,能完美避开新版Tokenizer的兼容性问题,避免出现“token mismatch”等报错。
2.2 模型下载与加载
ChatGLM3-6B-32K模型已预置在CSDN星图镜像中,但为确保可控性,我们推荐手动下载:
# 使用huggingface-cli(需先登录hf-cli) huggingface-cli download THUDM/chatglm3-6b-32k --local-dir ./chatglm3-6b-32k # 或使用git lfs(更稳定) git lfs install git clone https://huggingface.co/THUDM/chatglm3-6b-32k模型下载完成后,编写基础加载脚本load_model.py:
# load_model.py from transformers import AutoTokenizer, AutoModel import torch # 加载分词器和模型 tokenizer = AutoTokenizer.from_pretrained("./chatglm3-6b-32k", trust_remote_code=True) model = AutoModel.from_pretrained("./chatglm3-6b-32k", trust_remote_code=True) # 根据硬件选择设备 if torch.cuda.is_available(): model = model.cuda().half() # 半精度,显存占用约13GB print(" 模型已加载到GPU,使用半精度") else: model = model.float() print(" 无GPU,模型加载到CPU,推理速度较慢") model.eval() print(" 模型加载完成,准备就绪")运行该脚本,你会看到类似输出:
模型已加载到GPU,使用半精度 模型加载完成,准备就绪小贴士:如果你的显存不足13GB,可启用4-bit量化:
model = model.quantize(4).cuda() # 显存降至约7.6GB,性能损失极小
2.3 Streamlit Web界面快速启动(可选)
CSDN星图镜像已为你预装了Streamlit版对话系统。只需一行命令即可启动:
streamlit run /path/to/composite_demo/main.py但本教程重点在于工具调用的底层逻辑,因此我们后续将基于纯Python脚本开发,确保你理解每一步原理。
3. 工具注册:用Python函数定义AI能力
工具调用的核心思想非常朴素:把现实世界的功能,包装成标准格式的Python函数,然后告诉模型“你有这些能力可用”。
ChatGLM3采用了一种极简的装饰器注册方式,无需修改模型代码,也不用写JSON Schema。
3.1 创建工具注册模块
新建文件tool_registry.py:
# tool_registry.py from typing import Annotated, List, Dict, Any import json import requests from datetime import datetime # 工具注册装饰器(ChatGLM3原生支持) def register_tool(func): """注册函数为可调用工具""" if not hasattr(register_tool, 'tools'): register_tool.tools = {} register_tool.tools[func.__name__] = func return func # 示例1:天气查询工具 @register_tool def get_weather( city_name: Annotated[str, "城市名称,如'北京'、'上海'", True], ) -> str: """ 获取指定城市的未来7天天气预报。 返回格式:JSON字符串,包含日期、温度、天气状况。 """ # 模拟API调用(实际项目中替换为真实天气API) mock_data = { "city": city_name, "date": datetime.now().strftime("%Y-%m-%d"), "forecast": [ {"day": "今天", "temp": "22~28°C", "weather": "多云"}, {"day": "明天", "temp": "20~26°C", "weather": "小雨"}, {"day": "后天", "temp": "19~25°C", "weather": "晴"} ] } return json.dumps(mock_data, ensure_ascii=False, indent=2) # 示例2:数学计算工具 @register_tool def calculate( expression: Annotated[str, "合法的Python数学表达式,如'2+3*4'、'pow(2,10)'", True], ) -> str: """ 安全执行数学表达式计算。 支持基本运算符、math函数(如sin、cos、log)、幂运算。 """ try: # 仅允许安全的内置函数和math模块 allowed_names = { "__builtins__": {}, "abs": abs, "round": round, "max": max, "min": min, "sum": sum, "len": len, } # 添加常用math函数 import math for name in ['sin', 'cos', 'tan', 'log', 'log10', 'sqrt', 'pow', 'exp']: if hasattr(math, name): allowed_names[name] = getattr(math, name) result = eval(expression, {"__builtins__": {}}, allowed_names) return f"计算结果:{result}" except Exception as e: return f"计算错误:{str(e)}" # 示例3:当前时间工具 @register_tool def get_current_time() -> str: """ 获取当前系统时间。 返回格式:YYYY-MM-DD HH:MM:SS """ return datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 导出所有注册工具 def get_all_tools() -> Dict[str, Any]: """获取所有已注册工具的字典""" return getattr(register_tool, 'tools', {})3.2 工具注册原理说明
你可能好奇:为什么一个简单的装饰器就能让模型识别工具?关键在于ChatGLM3的System Prompt设计。
当模型启动时,我们会向它发送一段特殊的系统指令,其中包含了所有已注册工具的函数签名和文档字符串。模型通过学习大量工具调用样本,掌握了如何根据用户问题,从这些函数中选择最匹配的一个,并提取参数。
你完全不需要手动拼接JSON Schema,@register_tool装饰器会自动提取:
- 函数名 → 工具名称(
get_weather) - 参数名和Annotated注释 → 参数定义(
city_name: str+ 描述) - 函数docstring → 工具用途说明
这就是ChatGLM3“开箱即用”工具调用体验的底层魔法。
4. 工具调用全流程实现
现在进入核心环节:如何让模型真正调用你注册的函数?我们将构建一个完整的调用循环,包含四步:准备Prompt → 模型推理 → 解析调用 → 执行反馈。
4.1 构建带工具的System Prompt
新建文件tool_caller.py:
# tool_caller.py from typing import Dict, List, Any, Optional import json import re from tool_registry import get_all_tools def build_system_prompt() -> str: """ 构建包含所有工具信息的System Prompt 这是触发工具调用的关键 """ tools = get_all_tools() if not tools: return "你是一个通用AI助手,不支持外部工具调用。" # 将每个工具转换为自然语言描述 tool_descriptions = [] for name, func in tools.items(): doc = func.__doc__.strip() if func.__doc__ else "无描述" sig = str(func.__annotations__) tool_descriptions.append(f"- {name}({sig}): {doc}") return f"""你是一个强大的AI助手,具备调用外部工具的能力。你可以使用以下工具: {'\n'.join(tool_descriptions)} 调用工具的规则: 1. 当用户问题需要实时数据、计算、文件操作等超出语言模型能力时,必须调用工具 2. 只能调用上面列出的工具,不能虚构工具名 3. 调用格式严格为:<|tool_start|>{tool_name}({json.dumps(args, ensure_ascii=False)})<|tool_end|> 4. args必须是合法JSON对象,键名必须与函数参数名完全一致 5. 不要在调用工具后添加任何解释性文字 请直接输出调用指令,不要输出其他内容。""" def parse_tool_call(response: str) -> Optional[Dict[str, Any]]: """ 解析模型输出中的工具调用指令 返回:{"name": "get_weather", "args": {"city_name": "北京"}} """ # 匹配 <|tool_start|>get_weather({"city_name": "北京"})<|tool_end|> pattern = r'<\|tool_start\|>(\w+)\((\{.*?\})\)<\|tool_end\|>' match = re.search(pattern, response, re.DOTALL) if not match: return None tool_name = match.group(1) try: args = json.loads(match.group(2)) return {"name": tool_name, "args": args} except json.JSONDecodeError: return None def execute_tool_call(tool_call: Dict[str, Any]) -> str: """ 执行工具调用并返回结果 """ tools = get_all_tools() tool_name = tool_call["name"] if tool_name not in tools: return f"错误:未找到工具 '{tool_name}'" try: result = tools[tool_name](**tool_call["args"]) return str(result) except Exception as e: return f"工具执行错误:{str(e)}"4.2 完整调用循环
继续在tool_caller.py中添加主循环:
def chat_with_tools( model, tokenizer, user_input: str, history: List[Dict[str, str]] = None ) -> str: """ 支持工具调用的完整对话函数 """ if history is None: history = [] # 1. 构建System Prompt(含工具描述) system_prompt = build_system_prompt() # 2. 构建对话历史(ChatGLM3格式) messages = [{"role": "system", "content": system_prompt}] for msg in history: messages.append(msg) messages.append({"role": "user", "content": user_input}) # 3. 模型推理(流式输出可选) response, _ = model.chat(tokenizer, user_input, history=history, system=system_prompt) # 4. 检查是否需要工具调用 tool_call = parse_tool_call(response) if tool_call: print(f" 检测到工具调用:{tool_call['name']}({tool_call['args']})") # 5. 执行工具 tool_result = execute_tool_call(tool_call) print(f"⚙ 工具执行结果:{tool_result[:100]}...") # 6. 将工具结果作为assistant回复,继续对话 # 注意:这里我们模拟一次“工具调用-返回”循环 # 实际生产环境可递归调用直到无工具调用 final_response = f"我已为你调用{tool_call['name']}工具,结果如下:\n{tool_result}" return final_response else: # 无工具调用,直接返回模型回复 return response # 测试函数 if __name__ == "__main__": from load_model import tokenizer, model # 测试用例 test_cases = [ "北京今天天气怎么样?", "计算2的10次方是多少?", "现在几点了?", "你好,今天过得怎么样?" ] print(" 开始工具调用测试...\n") for i, case in enumerate(test_cases, 1): print(f" 测试 {i}: {case}") result = chat_with_tools(model, tokenizer, case) print(f" 回复: {result}\n{'─' * 50}\n")4.3 运行效果演示
保存并运行:
python tool_caller.py你将看到类似输出:
开始工具调用测试... 测试 1: 北京今天天气怎么样? 检测到工具调用:get_weather({'city_name': '北京'}) ⚙ 工具执行结果:{ "city": "北京", "date": "2024-06-15", "forecast": [ { "day": "今天", "temp": "22~28°C", "weather": "多云" }, ... ] } 回复: 我已为你调用get_weather工具,结果如下: { "city": "北京", "date": "2024-06-15", "forecast": [ { "day": "今天", "temp": "22~28°C", "weather": "多云" }, ... ] } ────────────────────────────────────────── 测试 4: 你好,今天过得怎么样? 回复: 你好!作为AI助手,我没有真实的感受,但我很乐意陪你聊天、解答问题或帮你完成各种任务。你今天过得怎么样呢?成功!模型准确识别了需要调用
get_weather,并正确提取了参数{"city_name": "北京"}。
5. 进阶技巧:提升工具调用稳定性与实用性
5.1 处理工具调用失败的优雅降级
现实中,工具可能因网络、参数错误等原因失败。我们来增强execute_tool_call函数:
# 在 tool_caller.py 中替换 execute_tool_call 函数 def execute_tool_call(tool_call: Dict[str, Any]) -> str: """ 执行工具调用并返回结果,支持错误处理和降级 """ tools = get_all_tools() tool_name = tool_call["name"] if tool_name not in tools: return f"抱歉,我暂时不支持'{tool_name}'这个功能。你可以试试问我天气、计算或时间相关的问题。" try: result = tools[tool_name](**tool_call["args"]) return str(result) except TypeError as e: # 参数类型错误 return f"参数错误:{str(e)}。请确认输入格式是否正确。" except Exception as e: # 其他异常 error_msg = str(e) if "timeout" in error_msg.lower() or "network" in error_msg.lower(): return "网络连接超时,请稍后再试。" else: return f"执行出错:{error_msg}。我会继续努力改进。"5.2 支持多轮工具调用(链式调用)
ChatGLM3-6B-32K支持32K上下文,完全可以处理复杂的多步骤任务。例如:“先查北京天气,再告诉我上海的温度”。
我们扩展主循环,支持最多2次工具调用:
def chat_with_tools_advanced( model, tokenizer, user_input: str, history: List[Dict[str, str]] = None, max_tool_calls: int = 2 ) -> str: """ 支持多轮工具调用的高级版本 """ if history is None: history = [] current_input = user_input full_history = history.copy() for call_count in range(max_tool_calls): # 构建当前轮次的prompt system_prompt = build_system_prompt() messages = [{"role": "system", "content": system_prompt}] messages.extend(full_history) messages.append({"role": "user", "content": current_input}) # 模型推理 response, _ = model.chat(tokenizer, current_input, history=full_history, system=system_prompt) # 解析工具调用 tool_call = parse_tool_call(response) if not tool_call: # 无工具调用,返回最终结果 return response # 执行工具 tool_result = execute_tool_call(tool_call) # 将工具调用和结果加入历史,用于下一轮 full_history.append({"role": "assistant", "content": response}) full_history.append({ "role": "observation", "content": f"工具 {tool_call['name']} 执行结果:{tool_result}" }) # 生成下一轮输入:让模型基于工具结果继续思考 current_input = f"基于以上工具执行结果,请给出最终回答。" # 达到最大调用次数仍未得到最终答案,返回最后一次模型输出 return response5.3 自定义工具开发指南
你想添加自己的工具?只需三步:
- 在
tool_registry.py中添加新函数 - 使用
@register_tool装饰 - 添加清晰的Annotated参数和docstring
例如,添加一个“读取本地文本文件”的工具:
# 在 tool_registry.py 中追加 import os @register_tool def read_file( file_path: Annotated[str, "要读取的文件路径,如'test.txt'或'data/log.txt'", True], ) -> str: """ 读取本地文本文件内容。 注意:仅支持当前工作目录及子目录下的文件。 """ try: if not os.path.exists(file_path): return f"错误:文件 '{file_path}' 不存在" with open(file_path, 'r', encoding='utf-8') as f: content = f.read(2000) # 限制长度,防止OOM return f"文件内容(前2000字符):\n{content}" except Exception as e: return f"读取文件失败:{str(e)}"然后在对话中问:“请读取readme.md文件的内容”,模型就会自动调用此函数。
6. 总结:从理论到落地的关键认知
回顾整个教程,你已经完成了ChatGLM3工具调用的完整闭环。但比代码更重要的是几个关键认知,它们决定了你能否在真实项目中用好这项技术:
6.1 工具调用不是万能的,而是有明确边界的
- 适合场景:需要实时数据(天气、股票)、精确计算(数学、金融)、文件操作(读写Excel)、系统交互(发邮件、控制IoT设备)
- 不适合场景:主观判断(“这个设计美吗?”)、开放创作(“写一首诗”)、模糊需求(“帮我做个好东西”)
工具调用的本质,是把确定性任务交给确定性程序,把不确定性留给人类。
6.2 提示词工程比模型本身更重要
你会发现,build_system_prompt()函数中的措辞,极大影响调用成功率。经过实测,以下技巧显著提升效果:
- 动词前置:用“调用”、“执行”、“获取”等强动作词,而非“可以”、“能够”
- 格式强调:多次重复
<|tool_start|>和<|tool_end|>标记,强化格式记忆 - 错误预防:明确禁止虚构工具、禁止添加解释文字,减少幻觉
6.3 本地部署带来的是真正的可控性与隐私保障
相比调用云端API,本地工具调用有不可替代的优势:
- 数据不出域:你的天气查询、文件内容、内部API密钥,全程在本地处理
- 断网可用:工厂内网、科研实验室、飞行途中,AI依然可靠
- 成本归零:无需API调用费用,一次部署,永久免费
这正是CSDN星图镜像所倡导的——把AI能力真正交还给使用者。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。