1. 项目概述当LLM学会“调用函数”如果你正在开发一个基于大语言模型LLM的应用比如一个智能客服、一个数据分析助手或者一个自动化工作流你大概率会遇到一个核心难题LLM的“幻觉”和“知识截止日期”。你希望它能查询实时天气、操作数据库、调用第三方API或者执行一个复杂的计算但LLM本身只是一个语言模型它无法直接“行动”。它只能“说”不能“做”。传统的解决思路是开发者需要编写大量的胶水代码解析LLM的回复判断其意图然后手动调用对应的函数再把结果塞回给LLM让它继续生成回复。这个过程繁琐、易错且难以维护。而sigoden/llm-functions这个项目正是为了解决这个痛点而生的。它不是一个庞大的框架而是一个精巧、实用的工具库其核心思想是让函数调用成为LLM的一种原生能力。简单来说llm-functions允许你将任意Python函数“描述”给LLMLLM在理解用户意图后可以自主决定是否调用、以及调用哪个函数并自动将函数的执行结果整合到其后续的对话或思考中。这极大地简化了构建“LLM工具”类应用即所谓的Agent或工具调用的流程。对于开发者而言你不再需要写复杂的意图识别和调度逻辑只需要定义好函数和它的自然语言描述剩下的交给llm-functions和LLM去协作完成。这个项目特别适合那些希望快速为LLM应用添加外部能力但又不想陷入复杂架构设计的开发者。无论是想给聊天机器人加一个查股票的功能还是让AI助手能帮你操作本地文件llm-functions都能提供一个清晰、直接的实现路径。2. 核心设计思路与架构解析2.1 从“意图识别”到“函数即能力”的范式转变在llm-functions出现之前主流的LLM工具调用方案可以概括为“两步走”意图识别与参数提取首先通过一个独立的LLM调用或规则引擎分析用户输入判断用户想做什么意图并提取出必要的参数如城市名、股票代码。手动调用与结果回填开发者根据识别出的意图在代码中手动调用对应的函数或API获取结果后再将这个结果作为新的上下文发送给LLM生成最终回复。这种方式的问题在于它把本应由LLM完成的“理解-决策-执行”链条人为地切断了。llm-functions的设计哲学是反其道而行之将函数的定义包括名称、描述、参数schema直接暴露给LLM让LLM在生成回复的“思考过程”中自主决定是否需要“外援”。这更贴近人类解决问题的过程——我们在思考时如果意识到需要查资料或使用计算器会自然地暂停思考去执行那个动作然后再继续。2.2 项目架构与核心组件llm-functions的架构非常简洁核心是围绕Function和FunctionRegistry两个概念构建的。Function类这是能力的载体。你定义的每一个Python函数都需要被包装成一个Function对象。这个包装过程不仅仅是简单的封装关键在于你需要为这个函数提供丰富的“元数据”名称name函数的唯一标识。描述description用自然语言清晰描述这个函数是做什么的。这是LLM理解该函数用途的关键。例如“获取指定城市的当前天气情况”就比“get_weather”好得多。参数Schemaparameters严格定义函数每个参数的类型、描述、是否必需等。这通常遵循JSON Schema格式确保了LLM能精确地生成符合要求的调用参数。FunctionRegistry类这是能力的仓库。它是一个中心化的注册表用于管理所有已注册的Function对象。当LLM需要决定调用哪个函数时它会查询这个注册表。注册表的设计支持动态添加和移除函数使得应用的能力可以灵活扩展。与LLM的集成层这是项目的“魔法”发生地。llm-functions并不绑定于某个特定的LLM提供商如OpenAI、Anthropic。它通过适配器模式将FunctionRegistry中的函数信息转换成特定LLM API所要求的“工具定义”Tool Definitions格式。例如对于OpenAI的Chat Completions API它会生成符合其tools参数要求的JSON对象。然后在LLM的回复中它会解析出tool_calls字段自动执行对应的函数并将结果以tool消息的形式追加到对话历史中供LLM进行下一轮推理。注意llm-functions本身不包含LLM的调用逻辑它只负责“定义函数”和“解析调用”。你需要结合像openai、anthropic或litellm这样的SDK来完成与LLM的实际通信。这使得它非常轻量且易于集成到现有项目中。2.3 对比主流方案与LangChain、LlamaIndex的异同你可能会问LangChain的Tools和AgentsLlamaIndex的Query Engines with Tools不也是做类似的事情吗是的但它们的设计目标和复杂度不同。LangChain是一个功能极其丰富的大框架涵盖了从数据加载、向量存储、记忆管理到链式调用、代理的方方面面。它的Tool调用功能是其中一环但学习曲线陡峭抽象层次多。如果你需要一个“全家桶”式的解决方案LangChain很强大。但如果你只想解决“让LLM调用我的几个函数”这个具体问题LangChain可能显得过于重型。LlamaIndex核心优势在于数据索引和检索RAG。它的工具调用功能更多是服务于其检索流程的增强。如果你的应用核心是文档问答需要结合工具调用LlamaIndex是不错的选择。llm-functions专注且极简。它只解决“函数调用”这一件事并且力求API设计直观、侵入性低。你不需要理解复杂的“Agent执行循环”、“ToolExecutor”等概念。它的代码量小依赖少更容易理解和调试。对于快速原型开发、中小型项目或者希望将工具调用能力嵌入到现有非LLM框架应用中的场景llm-functions的优势非常明显。选择建议如果你的项目复杂度高需要记忆、复杂的工作流、多种数据连接器选LangChain。如果你的核心是文档检索选LlamaIndex。如果你需要的是一个轻量、专注、可以像导入普通库一样轻松使用的函数调用工具llm-functions是你的理想选择。3. 核心细节解析与实操要点3.1 如何定义一个合格的“LLM可调用函数”定义一个能被LLM良好理解和调用的函数远不止写一个def那么简单。以下是需要关注的几个核心细节1. 函数签名与类型提示Type Hints强烈建议使用Python的类型提示。这不仅能让你的代码更清晰llm-functions也能利用这些信息自动生成更准确的参数Schema。例如def get_weather(city: str, country_code: str “CN”) - str: …这里的str类型和默认值”CN”都会被捕获帮助LLM理解country_code是一个可选的字符串参数。2. 描述Description的撰写艺术描述是LLM理解函数功能的唯一自然语言入口。一个好的描述应该准确明确说明函数的作用。例如“计算两个数的和”就比“处理数字”准确。完整提及关键的限制或前提条件。例如“根据股票代码查询实时股价仅支持沪深A股和部分美股代码”。用户意图导向从用户可能提出的问题角度来描述。例如“解答关于Python标准库的用法问题”可以对应函数search_python_docs(query: str)。3. 参数Schema的精细化定义虽然llm-functions可以自动从类型提示生成基础Schema但对于复杂参数手动定义是必要的。枚举类型如果参数只能取几个特定值一定要定义为枚举。这能极大提高LLM调用的准确性。from enum import Enum class Unit(Enum): CELSIUS “celsius” FAHRENHEIT “fahrenheit” def get_temperature(city: str, unit: Unit Unit.CELSIUS) - float: …在注册函数时llm-functions会将这个枚举信息传递给LLMLLM就知道unit只能填 “celsius” 或 “fahrenheit”。复杂对象如果参数是一个字典或Pydantic模型需要详细定义其内部结构。4. 函数的健壮性与错误处理LLM调用的函数必须非常健壮。因为用户的输入是开放域的LLM提取的参数可能五花八门。输入验证在函数内部开头务必对参数进行二次验证。例如即使Schema要求city是字符串LLM也可能传一个不存在的城市名。你的函数需要能处理这种情况并返回一个友好的错误信息如“未找到该城市信息”这个信息会被传回给LLM让它向用户解释。异常捕获函数内部所有可能抛出异常的地方如网络请求、数据库查询都必须用try…except包裹返回一个结构化的错误结果而不是让异常直接抛出导致整个调用链崩溃。3.2 FunctionRegistry 的管理策略FunctionRegistry作为中央仓库其管理方式直接影响应用的灵活性和性能。按模块/功能分组注册不要把所有函数都堆在一个地方。可以按功能模块创建多个注册表或者使用一个主注册表但通过命名前缀来分组例如weather.get、stock.query、file.read。这有助于管理和避免命名冲突。动态注册与热更新llm-functions支持在运行时动态添加或移除函数。这个特性非常有用例如你可以实现一个插件系统用户上传的插件可以将其函数动态注册到主应用中。或者在长期运行的服务中根据配置热加载新的工具函数。注册表的序列化与持久化虽然项目本身可能不直接提供但你可以很容易地将FunctionRegistry中的函数定义主要是元数据序列化为JSON或YAML保存。这在需要将函数配置与代码分离或者进行版本管理时非常有用。3.3 与不同LLM供应商的适配实战llm-functions通过bind方法或类似的适配器来连接不同的LLM。这里以OpenAI和Anthropic为例说明关键配置。OpenAI (gpt-3.5-turbo, gpt-4)OpenAI的API原生支持tools和tool_choice参数。llm-functions的适配器会将你的Function列表转换成OpenAI要求的格式。关键点在于tool_choice参数”auto”让模型自行决定是否调用函数以及调用哪个。这是最常用的模式。”none”禁止模型调用任何函数。{“type”: “function”, “function”: {“name”: “get_weather”}}强制模型调用指定的函数。这在调试或构建确定性工作流时有用。实操心得使用OpenAI时务必在系统提示System Prompt中给予模型清晰的指令引导它积极使用工具。例如“你是一个有帮助的助手可以调用各种工具来获取信息。当用户的问题需要实时数据、计算或外部信息时你应该调用相应的工具。”Anthropic (Claude)Anthropic的API设计略有不同它使用tools参数定义工具并在消息中通过特定的结构来触发工具调用。llm-functions的Anthropic适配器会处理这些转换。需要注意的是Claude对工具描述的格式可能更敏感确保描述简洁明了。通用建议无论使用哪个供应商在首次集成后一定要用一组典型的用户query进行测试观察LLM是否正确理解了函数描述、是否在合适的时机发起了调用、参数提取是否准确。这是一个迭代优化的过程。4. 完整实操流程构建一个天气查询助手下面我们通过一个完整的例子一步步展示如何使用llm-functions构建一个能查询天气的智能助手。我们将使用OpenAI的GPT-4作为LLM引擎。4.1 环境准备与安装首先创建一个新的Python虚拟环境并安装必要的包。# 创建并激活虚拟环境以conda为例 conda create -n llm-functions-demo python3.10 conda activate llm-functions-demo # 安装核心库和OpenAI SDK pip install llm-functions openai python-dotenv requestsllm-functions是核心库openai用于调用GPTpython-dotenv用于管理API密钥requests用于在我们的天气函数中发起HTTP请求。接下来在项目根目录创建.env文件存放你的OpenAI API密钥OPENAI_API_KEYsk-your-secret-key-here4.2 定义工具函数与注册我们创建weather_tool.py文件定义天气查询函数。# weather_tool.py import os import requests from typing import Optional from enum import Enum from dotenv import load_dotenv from llm_functions import Function # 加载环境变量 load_dotenv() # 模拟一个天气API的密钥这里用OpenWeatherMap示例你需要自己申请 WEATHER_API_KEY os.getenv(“WEATHER_API_KEY”, “your_fallback_key”) class TemperatureUnit(Enum): 温度单位枚举 CELSIUS “metric” # 对应API中的摄氏度参数 FAHRENHEIT “imperial” def get_current_weather( location: str, unit: TemperatureUnit TemperatureUnit.CELSIUS ) - str: 获取指定城市的当前天气情况。 参数: location: 城市名称例如“北京”、“New York”。支持中英文。 unit: 温度单位可选“CELSIUS”摄氏度或“FAHRENHEIT”华氏度默认为摄氏度。 返回: 一个描述天气的字符串包括温度、天气状况和湿度。 如果查询失败返回错误信息。 # 在实际项目中这里应调用真实的天气API如OpenWeatherMap # 此处为模拟逻辑 if not location: return “错误请提供有效的城市名称。” # 模拟API调用和响应解析 # 假设我们有一个模拟的API响应 mock_data { “Beijing”: {“temp”: 22, “condition”: “晴朗”, “humidity”: 65}, “New York”: {“temp”: 68, “condition”: “多云”, “humidity”: 80}, } city_key location.strip() if city_key in mock_data: data mock_data[city_key] temp data[“temp”] condition data[“condition”] humidity data[“humidity”] unit_str “°C” if unit TemperatureUnit.CELSIUS else “°F” return f”{location}的当前天气{condition}温度 {temp}{unit_str}湿度 {humidity}%。” else: return f”抱歉暂时无法获取 {location} 的天气信息。请检查城市名称是否正确。” # 将函数包装成LLM可识别的Function对象 weather_function Function.from_callable( get_current_weather, name“get_current_weather”, # 给函数一个明确的调用名 description“查询指定城市的实时天气。”, # 简洁的描述 )4.3 集成LLM并创建对话循环现在创建主程序main.py将函数注册并与OpenAI GPT进行交互。# main.py import os from openai import OpenAI from dotenv import load_dotenv from llm_functions import FunctionRegistry from weather_tool import weather_function # 加载环境变量和初始化 load_dotenv() client OpenAI(api_keyos.getenv(“OPENAI_API_KEY”)) # 1. 创建函数注册表并注册天气函数 registry FunctionRegistry() registry.register(weather_function) # 2. 将注册表绑定到OpenAI客户端生成工具定义 # llm_functions的OpenAI集成器会处理转换 from llm_functions.integrations.openai import OpenAIIntegration integration OpenAIIntegration(registry) # 这个 tools 变量就是可以直接传给OpenAI API的格式 tools integration.get_tools_for_openai() # 3. 系统提示引导模型使用工具 system_prompt “”” 你是一个智能天气助手专门回答与天气相关的问题。 你可以调用“get_current_weather”工具来查询全球城市的实时天气。 当用户询问某个地方的天气时你应该主动调用这个工具来获取准确信息。 如果工具返回了信息请用友好、自然的方式告诉用户。 如果工具返回错误请向用户解释可能的原因如城市名不对。 对于与天气无关的问题请礼貌地告知你的能力范围。 “”” # 4. 简单的对话循环 def chat_with_weather_assistant(): messages [{“role”: “system”, “content”: system_prompt}] print(“天气助手已启动输入‘退出’或‘quit’结束对话。\n”) while True: try: user_input input(“\n你: “).strip() if user_input.lower() in [“退出”, “quit”, “exit”]: print(“助手: 再见”) break # 将用户输入加入消息历史 messages.append({“role”: “user”, “content”: user_input}) # 调用OpenAI API传入工具定义 response client.chat.completions.create( model“gpt-4-turbo-preview”, # 或 gpt-3.5-turbo messagesmessages, toolstools, tool_choice“auto”, # 让模型自行决定是否调用工具 temperature0.2, # 较低的温度使输出更确定 ) # 获取模型的回复 assistant_message response.choices[0].message messages.append(assistant_message) # 先将模型的回复可能包含tool_calls加入历史 # 5. 检查模型是否想要调用工具 if assistant_message.tool_calls: print(f”助手: 正在查询天气信息…”) # 处理每一个工具调用本例中只有一个工具 for tool_call in assistant_message.tool_calls: function_name tool_call.function.name function_args json.loads(tool_call.function.arguments) # 通过集成器执行工具调用 # integration.execute_tool_call 会自动找到对应函数并传入参数 tool_result integration.execute_tool_call(tool_call) # 将工具执行结果作为一条新消息追加到历史中 messages.append({ “role”: “tool”, “tool_call_id”: tool_call.id, “content”: str(tool_result), # 结果需要是字符串 }) # 工具调用并添加结果后需要让模型基于新信息生成最终回复 second_response client.chat.completions.create( model“gpt-4-turbo-preview”, messagesmessages, # 此时历史已包含工具执行结果 ) final_message second_response.choices[0].message messages.append(final_message) print(f”助手: {final_message.content}”) else: # 模型没有调用工具直接输出回复 print(f”助手: {assistant_message.content}”) except Exception as e: print(f”发生错误: {e}”) # 可以选择清空当前轮次的消息避免错误上下文影响 messages.pop() # 移除导致错误的用户输入 print(“助手: 抱歉处理你的请求时出了点问题请再试一次。”) if __name__ “__main__”: import json # 需要导入json来解析参数 chat_with_weather_assistant()4.4 运行与测试运行python main.py你就可以开始与你的天气助手对话了。测试用例1正常查询你: 北京今天天气怎么样 助手: 正在查询天气信息… 助手: 北京的当前天气晴朗温度 22°C湿度 65%。测试用例2带单位的查询你: What‘s the weather in New York in Fahrenheit? 助手: 正在查询天气信息… 助手: New York的当前天气多云温度 68°F湿度 80%。测试用例3无关问题你: 讲个笑话吧。 助手: 我是一个专注于天气查询的助手目前还没有学习讲笑话的功能。你可以问我关于任何城市天气的问题测试用例4错误处理模拟你: 火星的天气如何 助手: 正在查询天气信息… 助手: 抱歉暂时无法获取 火星 的天气信息。请检查城市名称是否正确。通过这个完整的例子你可以看到核心的“意图识别-函数调用-结果整合”循环完全由llm-functions和 OpenAI API 协作处理我们只需要定义好函数和清晰的描述即可。这大大简化了开发流程。5. 高级应用与性能优化5.1 构建复杂多工具Agent一个实用的助手通常不止一个功能。我们可以轻松扩展注册表加入更多工具。示例添加股票查询和计算器# finance_tool.py from llm_functions import Function from typing import List def get_stock_price(symbol: str) - str: “”“查询指定股票代码的实时股价。仅支持部分主要股票代码如 AAPL, GOOGL。”“” # 模拟数据 mock_prices {“AAPL”: 172.3, “GOOGL”: 151.2, “MSFT”: 406.5} price mock_prices.get(symbol.upper()) if price: return f”{symbol} 的最新股价为 ${price}。” else: return f”未找到股票代码 {symbol} 的信息。” def calculate_expression(expression: str) - str: “”“计算一个数学表达式的结果。支持加减乘除和括号例如 ‘(23)*4’。”“” try: # 警告使用eval有安全风险仅作演示。生产环境应用安全计算库如ast.literal_eval或自定义解析器。 result eval(expression) return f”表达式 ‘{expression}’ 的计算结果是{result}。” except Exception as e: return f”无法计算表达式 ‘{expression}’{e}。” stock_function Function.from_callable(get_stock_price, name“query_stock”, description“查询美股的实时股价。”) calc_function Function.from_callable(calculate_expression, name“calculate”, description“计算一个数学表达式。”) # 在主程序中注册 registry.register(stock_function) registry.register(calc_function) # 记得更新 tools integration.get_tools_for_openai()现在你的助手就能回答“苹果股价多少”和“计算一下(1527)/3等于多少”这类问题了。关键在于系统提示要更新概括所有可用工具。5.2 流式输出Streaming与函数调用在真实应用中我们往往希望获得流式响应让用户更快地看到结果。当结合函数调用时流程会稍微复杂一些。核心思路发起一个开启流式输出的API调用。监听返回的数据块chunks。当检测到一个完整的tool_calls结构时暂停流式输出去执行函数。执行完毕后将结果作为新的消息再次发起一个可能是非流式的API调用让模型生成最终回复或者继续流式输出最终回复。llm-functions本身不处理流式逻辑但它的设计允许你在流式处理的回调函数中使用integration.execute_tool_call来执行工具。你需要根据OpenAI流式返回的数据结构自己解析出完整的tool_calls对象。这是一个进阶话题需要对OpenAI的流式响应格式有深入了解。5.3 错误处理与用户反馈优化1. 函数执行失败函数内部应返回结构化的错误信息而不是抛出异常。LLM可以理解这些错误信息并生成友好的用户提示。例如在天气函数中返回“网络请求超时请稍后重试”。2. LLM参数提取错误有时LLM可能误解用户意图提取出错误的参数。例如用户问“上海和北京的天气”LLM可能只调用了get_weather(location”上海和北京”)。为了处理这种情况你有两个策略在函数内部做兼容处理例如尝试分割字符串或调用模糊搜索API。优化函数描述和系统提示在描述中明确“一次只能查询一个城市”并在系统提示中强调这一点。3. 工具选择不当如果LLM频繁错误地选择工具或者该用工具时不用需要调整优化工具描述使其更精确与其他工具区分度更高。调整系统提示更明确地指导模型何时使用工具。提供少量示例Few-Shot在系统提示或初始消息中给出一两个正确使用工具的对话示例这是引导LLM行为最有效的方法之一。6. 常见问题与排查技巧实录在实际使用llm-functions的过程中你可能会遇到一些典型问题。下面是一个快速排查指南。问题现象可能原因排查步骤与解决方案LLM完全不调用函数1. 系统提示未引导使用工具。2. 函数描述不清晰或与用户问题不匹配。3.tool_choice参数被误设为”none”。1. 检查并强化系统提示明确要求模型在需要时使用工具。2. 重写函数描述使其更贴近用户自然提问方式。3. 确认API调用中tool_choice”auto”。LLM调用了函数但参数总是错误1. 参数Schema定义不准确如类型、枚举值。2. 函数描述未明确参数格式如日期格式“YYYY-MM-DD”。3. LLM模型能力不足可尝试换更强模型。1. 使用Python类型提示和Pydantic模型精确定义Schema。2. 在参数描述中写明格式要求。3. 在系统提示中通过示例Few-Shot展示正确参数格式。函数被调用但返回结果后LLM的回复不理想1. 函数返回的结果不是纯文本或格式混乱。2. LLM未能正确理解函数返回的上下文。1. 确保函数返回字符串。复杂数据应格式化成易读的自然语言。2. 在函数返回的字符串中可以包含一些引导性语句如“查询结果显示…”。遇到KeyError或AttributeError提示找不到函数1. 函数注册的名称与LLM调用时使用的名称不一致。2. 函数未正确注册到FunctionRegistry。1. 检查Function.from_callable的name参数确保与LLMtool_calls中的function.name完全一致大小写敏感。2. 确认registry.register()已成功执行并且注册表实例被正确传递给了集成器。流式输出模式下工具调用逻辑混乱流式响应中tool_calls是分块传输的需要组装完整后才能执行。1. 参考OpenAI官方文档编写代码累积tool_calls的增量数据直到获得一个完整对象。2. 考虑在简单场景下先使用非流式模式功能稳定后再升级到流式。性能问题简单问题也触发函数调用增加延迟和成本LLM过于“积极”地使用工具。1. 调整系统提示说明“仅在必要时”使用工具。2. 对于非常简单的、模型自身知识就能完美回答的问题如“你好”可以在客户端逻辑中做前置过滤不触发工具调用流程。实操心得一描述即契约把函数的自然语言描述看作是与LLM签订的“契约”。描述越精准LLM履约就越准确。花时间反复打磨每个函数的描述并让不同的人阅读看是否会产生歧义这项投入的回报率极高。实操心得二测试驱动开发不要等到所有功能都写完再测试。为每个工具函数编写单元测试模拟LLM可能传入的各种边界和错误参数。同时构建一个端到端的测试集包含各种典型的用户提问自动化运行并检查LLM的最终输出是否符合预期。这能帮你快速发现描述不清、Schema定义错误等问题。实操心得三成本与延迟监控每次函数调用都意味着至少两次LLM API请求第一次决定调用第二次基于结果生成回复。对于高频应用这会使成本和响应时间翻倍。务必监控token消耗和响应延迟。对于一些结果稳定、耗时的查询如复杂计算可以考虑引入缓存机制将(函数名参数)哈希后缓存结果在一定时间内相同的请求直接返回缓存避免重复调用真实函数和后续的LLM生成。llm-functions就像给LLM这个“大脑”装上了可随意插拔的“手脚”。它剥离了复杂框架的厚重外壳将函数调用这一核心能力以最简洁的方式呈现出来。这种设计使得开发者能够更专注于定义“做什么”函数逻辑而将“何时做”和“如何调用”的复杂决策交给了更擅长此道的LLM。对于追求开发效率、热爱简洁设计的开发者来说这个项目无疑提供了一条构建智能应用的快车道。