1. 项目概述当大模型学会“调用函数”如果你最近在折腾大语言模型LLM的应用开发特别是想让模型去调用外部工具或API来完成一些复杂任务那你大概率遇到过这个场景你费尽心思写了一大段提示词Prompt告诉模型“这是一个天气查询函数它的参数是城市名返回天气信息”结果模型要么理解错了参数格式要么直接给你编造一个结果。这种“幻觉”和“不听话”的问题是LLM应用落地的一大痛点。sigoden/llm-functions这个项目就是为了解决这个痛点而生的。简单来说它是一套工具能让你用代码比如Python的Type Hints来定义函数然后自动生成高质量的提示词引导LLM如GPT-4、Claude、本地部署的Llama等正确地理解并调用这些函数。它把“人绞尽脑汁写提示词”的过程变成了“框架根据代码自动生成结构化指令”的过程。这听起来可能有点抽象我举个例子以前你需要手动告诉模型“请用JSON格式回复包含city和unit两个键”现在你只需要写一个Python函数def get_weather(city: str, unit: Literal[“celsius”, “fahrenheit”] “celsius”) - str:框架会自动帮你把函数签名、参数类型、描述等信息转换成模型能精准理解的“调用规范”。这个项目适合谁呢首先是所有正在构建基于LLM的智能体Agent、聊天机器人或自动化工作流的开发者。当你需要模型去执行搜索、查数据库、发邮件、操作软件等具体动作时llm-functions能极大提升开发效率和调用可靠性。其次它也适合那些对提示工程感到头疼的研究者或爱好者它能帮你省去大量调试提示词的繁琐工作让你更专注于业务逻辑本身。2. 核心设计思路从“自然语言描述”到“结构化契约”为什么传统的提示词方法在函数调用上容易失败核心原因在于自然语言本身具有模糊性而函数调用要求极高的精确性。你写的“城市名”模型可能理解为“北京”、“北京市”甚至“帝都”而你的后端API可能只接受标准的行政区划代码。llm-functions的设计哲学就是在这两者之间建立一个坚固、无歧义的“结构化契约”。2.1 基于代码即契约Code as Contract的自动化项目的核心思路非常巧妙利用现代编程语言特别是Python的类型注解Type Hints和文档字符串Docstring作为信息源。这些本就是开发者写给机器和同事看的、结构清晰的“契约”。llm-functions读取这些信息并将其转化为两种LLM更容易处理的形式结构化提示词Structured Prompt它会将函数名、每个参数的名称、类型、默认值以及可选的描述整合成一段格式严谨的系统指令。例如“你是一个助手可以调用工具。工具get_weather用于查询天气它接受一个字符串参数city表示城市名和一个可选参数unit只能是‘celsius’或‘fahrenheit’默认是‘celsius’。请仅在需要查询天气时以指定JSON格式调用此工具。”JSON Schema这是更机器友好的描述方式明确定义了函数调用请求体必须遵循的JSON结构包括function、arguments等字段以及arguments内部每个参数的类型、枚举值等约束。这为后续的请求解析和验证提供了直接依据。这种方式的好处是显而易见的。首先它保证了信息的一致性。你的代码是唯一的真相来源提示词自动生成避免了手动编写可能出现的笔误或描述偏差。其次它极大地提升了开发效率。增加一个参数改一下类型注解提示词和调用规范自动更新。最后它降低了认知负荷开发者不需要在“编程思维”和“自然语言提示词思维”之间来回切换。2.2 与主流方案的对比为什么是它在LLM函数调用领域OpenAI的Function Calling是事实上的标准LangChain、LlamaIndex等框架也提供了相关工具。那么llm-functions的独特价值在哪里轻量与专注它不试图成为一个全功能的AI应用框架而是专注于解决“函数定义与调用”这一个具体问题。这意味着它的API更简洁依赖更少更容易集成到现有项目中无论是FastAPI后端还是一个简单的脚本。模型无关性虽然OpenAI的Function Calling体验很好但它绑定于OpenAI的模型。llm-functions生成的是通用的结构化提示或JSON Schema你可以将其用于GPT、Claude、通义千问、DeepSeek甚至是本地部署的开源模型只要它们具备一定的指令遵循和JSON格式理解能力。本地优先与隐私整个定义和生成过程完全在本地完成无需将你的函数签名发送到任何外部服务用于生成提示词。这对于处理敏感业务逻辑或处于严格数据合规环境下的项目至关重要。对Python生态的深度集成它充分利用了Python的类型系统typing模块支持List[str]、Optional[int]、Literal[“A”, “B”]等复杂类型并能从pydantic的BaseModel中提取更丰富的字段描述和验证规则这是很多其他工具做得不够细致的地方。在我自己的项目中当我需要快速原型验证一个涉及多个工具调用的智能体并且不希望被某个特定云服务或重型框架锁定时llm-functions往往是我的首选。3. 核心细节解析与实操要点理解了设计思路我们来看看具体怎么用。项目主要围绕几个核心概念展开函数定义、提示词生成、调用解析。3.1 函数定义不仅仅是类型提示定义一个能被llm-functions良好处理的函数你需要有意识地使用类型注解和文档字符串。from typing import Literal from pydantic import BaseModel, Field # 示例1基础函数 def search_web(query: str, max_results: int 5) - list[str]: 使用搜索引擎进行网页搜索。 Args: query: 搜索关键词应尽可能具体。 max_results: 返回的最大结果数量默认为5最多不超过10。 Returns: 一个包含搜索结果摘要的字符串列表。 # ... 实际的搜索逻辑 ... pass # 示例2使用Pydantic模型定义复杂参数 class BookingInfo(BaseModel): city: str Field(description目的地城市名称) check_in_date: str Field(description入住日期格式为YYYY-MM-DD) nights: int Field(gt0, le30, description入住晚数) room_type: Literal[standard, deluxe, suite] standard def book_hotel(booking: BookingInfo) - str: 根据提供的预订信息创建酒店订单。 # ... 实际的预订逻辑 ... pass实操要点与心得描述Description是关键虽然类型如str告诉了模型参数是字符串但description才真正告诉了模型这个字符串应该是什么。好的描述应简洁、无歧义并可能包含格式要求如日期格式或示例。llm-functions会优先使用Field中的description其次是文档字符串中的Args部分。善用Literal和Enum对于有限选项的参数一定要用Literal或Enum。这能极大减少模型的“胡思乱想”。比如unit: Literal[“celsius”, “fahrenheit”]比unit: str然后靠描述说明要好得多。复杂结构用Pydantic当参数本身是一个复杂对象如预订信息包含多个字段时将其定义为Pydantic的BaseModel是最佳实践。这不仅能生成清晰的Schema还能利用Pydantic进行后续的数据验证。返回值类型注解虽然LLM不直接执行返回的代码但清晰的返回类型如- dict或- list[str]有助于框架生成更完整的工具描述有时也能辅助后续处理。3.2 提示词生成与系统消息构建定义好函数后下一步就是生成能让LLM理解的指令。llm-functions提供了llm_functions装饰器或FunctionRegistry类来收集函数并生成系统消息。from llm_functions import llm_functions, FunctionRegistry # 方法1使用装饰器简洁适合函数数量少且固定的场景 llm_functions class MyTools: def get_weather(self, city: str) - str: ... def send_email(self, to: str, subject: str, body: str) - bool: ... # 生成系统消息 tools MyTools() system_message tools.to_system_message() # 输出是一个字符串包含了所有函数的描述和调用格式说明 # 方法2使用FunctionRegistry灵活适合动态注册函数 registry FunctionRegistry() registry.register(search_web) registry.register(book_hotel) system_message registry.to_system_message() # 或者生成OpenAI格式的工具描述列表 openai_tools registry.to_openai_tools()生成的system_message可以直接作为对话历史中的第一条系统消息发送给LLM。openai_tools则是一个列表可以直接用于OpenAI API的tools参数。注意事项消息长度如果你注册了很多函数生成的系统消息可能会非常长消耗大量Tokens。在实际应用中需要根据场景精选必要的函数或者考虑分拆成多个具有不同职能的智能体。模型兼容性对于非OpenAI的模型to_system_message()生成的纯文本指令通用性更好。你需要测试你的目标模型是否能很好地理解这种格式。有时可能需要手动微调一下生成的提示词模板。函数名和参数名的可读性使用清晰、英文小写加下划线的函数名和参数名如calculate_monthly_revenue这本身就能被LLM很好地理解减少对描述的依赖。3.3 调用解析与响应处理LLM在理解了工具后会在回复中尝试以特定格式通常是JSON发起函数调用。你的应用需要解析这个回复执行对应的函数并将结果返回给LLM让它生成最终面向用户的回答。import json from llm_functions import FunctionRegistry # 假设我们已经有了registry和定义好的函数 registry FunctionRegistry() registry.register(get_weather) # 模拟LLM的回复在实际中这是你从API收到的响应 llm_response_text 我需要查询天气。我将调用get_weather函数。 json {function: get_weather, arguments: {city: 北京}}1. 解析LLM回复提取函数调用请求这里需要一个简单的解析器来从回复文本中提取JSON块。llm-functions可能提供或推荐一些辅助解析方法。import re json_match re.search(rjson\n(.*?)\n, llm_response_text, re.DOTALL) if json_match: call_request json.loads(json_match.group(1)) func_name call_request.get(function) func_args call_request.get(arguments, {})# 2. 从注册表中获取函数并调用 func registry.get_function(func_name) if func: try: # 执行实际函数 result func(**func_args) # 将结果格式化为LLM可继续处理的文本 execution_result f函数 {func_name} 执行成功返回结果{result} except Exception as e: execution_result f函数 {func_name} 执行失败错误{str(e)} else: execution_result f未知函数{func_name}else: # LLM没有调用函数是普通回复 execution_result None3. 将执行结果作为新的上下文再次发送给LLM让它生成最终回答。完整的对话历史可能是[系统消息, 用户问题, LLM第一次回复(含函数调用), 函数执行结果]然后请求LLM基于所有历史生成最终答案。**核心环节与避坑技巧** * **解析的鲁棒性**LLM的回复格式不一定完全标准。它可能把JSON放在代码块里也可能直接输出JSON字符串甚至可能在JSON前后加一些解释性文字。你需要编写一个健壮的解析器比如用正则表达式匹配{}或json块并做好错误处理。一些更完善的框架或llm-functions的高阶用法可能会封装这部分逻辑。 * **参数验证与转换**即使LLM理解了Schema它提供的参数值也可能不符合要求比如字符串传入数字。**强烈建议在执行函数前用Pydantic模型对arguments字典进行验证和解析**。这能提前捕获类型错误避免函数内部报出难以理解的异常。 python # 假设函数已用Pydantic模型定义参数 class WeatherParams(BaseModel): city: str unit: Literal[celsius, fahrenheit] celsius # 在调用函数前 validated_args WeatherParams(**func_args).dict() result get_weather(**validated_args) * **错误处理与用户反馈**函数执行可能失败网络超时、API限流、参数无效。不要把原始的异常堆栈直接扔给LLM或用户。应该捕获异常生成一个对用户或LLM友好的错误信息如“查询天气服务暂时不可用请稍后再试”并将其作为函数执行结果返回给LLM让它组织语言告知用户。 * **上下文管理**这是一个典型的多轮对话流程。你需要维护一个对话历史列表每次都将最新的用户输入、LLM回复、函数执行结果追加进去并在下一次请求时完整发送。注意Token数量的限制对于长对话可能需要做摘要或选择性遗忘。 ## 4. 实战构建一个多功能查询助手 让我们通过一个更完整的例子串联起所有环节。我们将构建一个简单的智能助手它可以查询天气和解释名词。 ### 4.1 定义工具函数 首先我们定义两个工具函数。为了模拟真实环境我们使用假的实现。 python import random from typing import List from pydantic import BaseModel, Field from datetime import date # 工具1查询天气 def get_weather(city: str, date_str: str None) - str: 获取指定城市未来三天的天气预报。 Args: city: 城市名称例如“上海”、“New York”。 date_str: 查询日期格式YYYY-MM-DD。默认为今天。 # 模拟数据 forecasts [晴, 多云, 小雨, 阴, 雷阵雨] if date_str: query_date date_str else: query_date str(date.today()) # 模拟返回三天的天气 result [f{query_date}今天: {random.choice(forecasts)}] for i in range(1, 3): future_date f未来第{i}天 result.append(f{future_date}: {random.choice(forecasts)}) return f{city}的天气预报\n \n.join(result) # 工具2解释名词使用Pydantic模型 class ExplanationRequest(BaseModel): term: str Field(description需要解释的专业术语或名词) audience: str Field( description解释的目标受众, defaultgeneral, regex^(general|student|expert)$ ) def explain_term(request: ExplanationRequest) - str: 向不同受众解释一个专业术语。 explanations { general: f‘{request.term}’是一个专业概念简单来说它指的是...通俗解释, student: f‘{request.term}’在学科中的定义是...它的主要特点是...教学式解释, expert: f关于‘{request.term}’其最新研究进展包括...当前面临的挑战是...深入探讨, } return explanations.get(request.audience, 未知受众类型。)4.2 创建函数注册表并生成提示from llm_functions import FunctionRegistry # 创建注册表并注册函数 registry FunctionRegistry() registry.register(get_weather) registry.register(explain_term) # 注意这里注册的是函数本身框架会自动处理其参数模型 # 生成系统消息 system_prompt registry.to_system_message() print( 生成的系统提示部分) print(system_prompt[:500]) # 打印前500字符预览生成的system_prompt会是一段详细的文本告诉LLM“你是一个助手可以调用以下工具1.get_weather工具用于...调用格式是...2.explain_term工具用于...调用格式是...”4.3 模拟与LLM的交互循环这里我们模拟一个简化版的交互流程。在实际中你需要连接真实的LLM API。import json import re class SimpleLLMAssistant: def __init__(self, registry): self.registry registry self.conversation_history [ {role: system, content: registry.to_system_message()} ] def _parse_function_call(self, llm_text: str): 一个简单的解析器从LLM回复中提取JSON函数调用。 # 尝试匹配 json ... 代码块 pattern rjson\s*(.*?)\s* match re.search(pattern, llm_text, re.DOTALL) if match: try: return json.loads(match.group(1)) except json.JSONDecodeError: pass # 如果没有代码块尝试直接找最外层的 {...} # 这是一个更简单但可能不稳定的方法 try: start llm_text.find({) end llm_text.rfind(}) 1 if start ! -1 and end ! 0: return json.loads(llm_text[start:end]) except: pass return None def chat_round(self, user_input: str): 处理一轮用户输入。 # 1. 将用户输入加入历史 self.conversation_history.append({role: user, content: user_input}) # 2. 模拟调用LLM这里用假响应代替真实API调用 # 在实际中这里是将self.conversation_history发送给GPT/Claude等API print(f\n[用户] {user_input}) # 为了演示我们根据用户输入硬编码一个“智能”的LLM回复 llm_response if 天气 in user_input: llm_response 用户想查询天气。我需要调用get_weather函数。 json {function: get_weather, arguments: {city: 上海}} elif 解释 in user_input or 什么是 in user_input: llm_response 用户需要解释一个术语。我需要调用explain_term函数。{function: explain_term, arguments: {term: 机器学习, audience: general}} else: llm_response 我目前只能帮您查询天气或解释名词请告诉我您需要哪方面的帮助print(f[LLM原始回复]\n{llm_response}) self.conversation_history.append({role: assistant, content: llm_response}) # 3. 解析LLM回复看是否包含函数调用 call_request self._parse_function_call(llm_response) if call_request: func_name call_request.get(function) func_args call_request.get(arguments, {}) print(f检测到函数调用请求: {func_name}, 参数: {func_args}) # 4. 执行函数 func self.registry.get_function(func_name) if func: try: # 对于使用Pydantic参数的函数需要特殊处理 if func_name explain_term: # explain_term 接受一个Pydantic模型实例 from pydantic import ValidationError try: req ExplanationRequest(**func_args) result func(req) except ValidationError as e: result f参数错误: {e} else: # get_weather 接受普通参数 result func(**func_args) execution_result f工具调用成功。结果{result} except Exception as e: execution_result f工具调用失败{str(e)} else: execution_result f未知工具{func_name} print(f[函数执行结果] {execution_result}) # 将执行结果加入历史让LLM生成最终回答 self.conversation_history.append({role: function, name: func_name, content: execution_result}) # 5. 再次“模拟”LLM基于结果生成最终回答 # 这里简单地将结果直接作为最终回复 final_reply f根据查询结果\n{execution_result.split(结果)[-1]} self.conversation_history.append({role: assistant, content: final_reply}) print(f[助手最终回复] {final_reply}) return final_reply else: # LLM没有调用函数直接返回其回复 print(f[助手回复] {llm_response}) return llm_response运行助手assistant SimpleLLMAssistant(registry) assistant.chat_round(今天上海天气怎么样) assistant.chat_round(请向普通人解释一下‘神经网络’)这个例子虽然简化了LLM调用部分但完整展示了从函数定义、提示生成、调用解析到执行的闭环。在实际项目中你需要将模拟LLM的部分替换为对真实API如OpenAI、Anthropic或本地模型的调用。 ## 5. 高级技巧与常见问题排查 在实际集成llm-functions到生产环境时你会遇到一些更具体的问题。这里分享一些经验和解决方案。 ### 5.1 处理复杂参数与嵌套结构 当你的函数参数是一个包含列表、嵌套对象的复杂结构时清晰的类型提示至关重要。 python from typing import List, Optional from pydantic import BaseModel, Field class Item(BaseModel): name: str Field(description商品名称) quantity: int Field(gt0, description购买数量) class OrderRequest(BaseModel): order_id: str Field(description订单号) items: List[Item] Field(description商品清单) shipping_address: Optional[str] Field(None, description收货地址如不填则使用默认地址) def place_order(request: OrderRequest) - str: 提交一个新订单。 # 业务逻辑 total_items sum(item.quantity for item in request.items) return f订单 {request.order_id} 已创建共 {total_items} 件商品。技巧对于嵌套的List[Item]llm-functions能很好地生成对应的JSON Schema告知LLMitems应该是一个对象数组每个对象有name和quantity字段。这比用自然语言描述“一个商品列表每个商品有名称和数量”要精确得多。5.2 动态函数注册与上下文感知有时你希望根据对话上下文或用户权限动态地提供不同的工具集。registry_basic FunctionRegistry() registry_basic.register(get_weather) registry_admin FunctionRegistry() registry_admin.register(get_weather) registry_admin.register(place_order) # ... 注册其他管理函数 def get_registry_for_user(user_role: str) - FunctionRegistry: if user_role admin: return registry_admin else: return registry_basic # 在每次会话开始时根据用户决定使用哪个注册表并生成对应的系统消息。5.3 常见问题与排查清单问题现象可能原因排查步骤与解决方案LLM完全不调用函数1. 系统提示词未正确发送或格式被破坏。2. 模型能力不足无法理解函数调用指令。3. 用户问题过于简单模型认为无需调用工具。1. 检查发送给API的system消息内容确保是完整的to_system_message()输出。2. 尝试使用能力更强的模型如GPT-4。对于开源模型检查其是否经过函数调用指令微调。3. 在系统提示词中更明确地要求模型“在需要时务必调用工具”。LLM调用了函数但参数错误1. 参数描述不清。2. 参数类型定义不严格如该用Literal却用了str。3. LLM的“幻觉”自行编造了不存在的参数。1. 为每个参数添加清晰、无歧义的description。2. 使用Literal,Enum或constr(regex...)严格限定取值范围或格式。3. 在解析后、执行前使用Pydantic进行强制验证和类型转换并给LLM反馈错误。解析JSON失败1. LLM的回复格式不符合预期如缺少代码块标记。2. JSON格式错误如缺少引号。1. 强化你的解析器支持多种格式如带或不带json标记。br2. 在系统提示词中明确要求LLM“将函数调用请求放在一个独立的json代码块中”。br3. 可以尝试使用LLM本身来修复格式或者使用json.loads的strict参数并做好异常捕获。函数执行结果未被LLM正确利用1. 返回给LLM的函数执行结果格式不佳。2. 对话历史管理混乱LLM丢失了上下文。1. 将函数结果格式化为清晰、简洁的文本。避免返回过长的原始数据或HTML。2. 确保将{role: function, name: ..., content: ...}这条消息正确地追加到对话历史中并随下一次请求发送。Token消耗过快1. 注册的函数过多导致系统提示词过长。2. 对话历史累积过长。1. 按需动态加载函数或对函数描述进行精简但需谨慎避免影响模型理解。2. 对长对话历史进行摘要Summarization或只保留最近N轮对话。5.4 性能与优化建议缓存提示词如果你的工具集是静态的不要每次请求都重新生成系统提示词。在应用启动时生成一次并缓存起来。异步执行如果函数调用涉及网络I/O如调用外部API务必使用异步函数async def并在异步框架如FastAPI、Sanic中执行避免阻塞整个应用。超时与重试为函数调用设置超时并考虑对可能失败的调用如网络请求实现重试机制。结构化结果虽然函数返回给LLM的是文本但你的内部处理可以也应该使用结构化数据。让函数返回字典或Pydantic模型对象然后在传递给LLM前序列化成易读的文本。这样既方便内部处理也便于LLM理解。sigoden/llm-functions这个项目就像给大模型装上了一套标准化的“操作手册”和“遥控器”。它通过代码这一精准的媒介极大地弥合了人类意图与机器执行之间的鸿沟。从我自己的使用体验来看它特别适合那些需要快速迭代、对模型兼容性有要求、或者希望保持技术栈轻量简洁的项目。当然它不是一个全能的Agent框架在复杂的流程编排、记忆管理等方面你可能还需要结合其他库。但无论如何在解决“让LLM可靠调用函数”这个核心问题上它提供了一种非常优雅且高效的方案。如果你正在为提示词调试而烦恼不妨试试它或许能帮你省下不少时间。
LLM函数调用实战:用llm-functions实现大模型精准工具调用
1. 项目概述当大模型学会“调用函数”如果你最近在折腾大语言模型LLM的应用开发特别是想让模型去调用外部工具或API来完成一些复杂任务那你大概率遇到过这个场景你费尽心思写了一大段提示词Prompt告诉模型“这是一个天气查询函数它的参数是城市名返回天气信息”结果模型要么理解错了参数格式要么直接给你编造一个结果。这种“幻觉”和“不听话”的问题是LLM应用落地的一大痛点。sigoden/llm-functions这个项目就是为了解决这个痛点而生的。简单来说它是一套工具能让你用代码比如Python的Type Hints来定义函数然后自动生成高质量的提示词引导LLM如GPT-4、Claude、本地部署的Llama等正确地理解并调用这些函数。它把“人绞尽脑汁写提示词”的过程变成了“框架根据代码自动生成结构化指令”的过程。这听起来可能有点抽象我举个例子以前你需要手动告诉模型“请用JSON格式回复包含city和unit两个键”现在你只需要写一个Python函数def get_weather(city: str, unit: Literal[“celsius”, “fahrenheit”] “celsius”) - str:框架会自动帮你把函数签名、参数类型、描述等信息转换成模型能精准理解的“调用规范”。这个项目适合谁呢首先是所有正在构建基于LLM的智能体Agent、聊天机器人或自动化工作流的开发者。当你需要模型去执行搜索、查数据库、发邮件、操作软件等具体动作时llm-functions能极大提升开发效率和调用可靠性。其次它也适合那些对提示工程感到头疼的研究者或爱好者它能帮你省去大量调试提示词的繁琐工作让你更专注于业务逻辑本身。2. 核心设计思路从“自然语言描述”到“结构化契约”为什么传统的提示词方法在函数调用上容易失败核心原因在于自然语言本身具有模糊性而函数调用要求极高的精确性。你写的“城市名”模型可能理解为“北京”、“北京市”甚至“帝都”而你的后端API可能只接受标准的行政区划代码。llm-functions的设计哲学就是在这两者之间建立一个坚固、无歧义的“结构化契约”。2.1 基于代码即契约Code as Contract的自动化项目的核心思路非常巧妙利用现代编程语言特别是Python的类型注解Type Hints和文档字符串Docstring作为信息源。这些本就是开发者写给机器和同事看的、结构清晰的“契约”。llm-functions读取这些信息并将其转化为两种LLM更容易处理的形式结构化提示词Structured Prompt它会将函数名、每个参数的名称、类型、默认值以及可选的描述整合成一段格式严谨的系统指令。例如“你是一个助手可以调用工具。工具get_weather用于查询天气它接受一个字符串参数city表示城市名和一个可选参数unit只能是‘celsius’或‘fahrenheit’默认是‘celsius’。请仅在需要查询天气时以指定JSON格式调用此工具。”JSON Schema这是更机器友好的描述方式明确定义了函数调用请求体必须遵循的JSON结构包括function、arguments等字段以及arguments内部每个参数的类型、枚举值等约束。这为后续的请求解析和验证提供了直接依据。这种方式的好处是显而易见的。首先它保证了信息的一致性。你的代码是唯一的真相来源提示词自动生成避免了手动编写可能出现的笔误或描述偏差。其次它极大地提升了开发效率。增加一个参数改一下类型注解提示词和调用规范自动更新。最后它降低了认知负荷开发者不需要在“编程思维”和“自然语言提示词思维”之间来回切换。2.2 与主流方案的对比为什么是它在LLM函数调用领域OpenAI的Function Calling是事实上的标准LangChain、LlamaIndex等框架也提供了相关工具。那么llm-functions的独特价值在哪里轻量与专注它不试图成为一个全功能的AI应用框架而是专注于解决“函数定义与调用”这一个具体问题。这意味着它的API更简洁依赖更少更容易集成到现有项目中无论是FastAPI后端还是一个简单的脚本。模型无关性虽然OpenAI的Function Calling体验很好但它绑定于OpenAI的模型。llm-functions生成的是通用的结构化提示或JSON Schema你可以将其用于GPT、Claude、通义千问、DeepSeek甚至是本地部署的开源模型只要它们具备一定的指令遵循和JSON格式理解能力。本地优先与隐私整个定义和生成过程完全在本地完成无需将你的函数签名发送到任何外部服务用于生成提示词。这对于处理敏感业务逻辑或处于严格数据合规环境下的项目至关重要。对Python生态的深度集成它充分利用了Python的类型系统typing模块支持List[str]、Optional[int]、Literal[“A”, “B”]等复杂类型并能从pydantic的BaseModel中提取更丰富的字段描述和验证规则这是很多其他工具做得不够细致的地方。在我自己的项目中当我需要快速原型验证一个涉及多个工具调用的智能体并且不希望被某个特定云服务或重型框架锁定时llm-functions往往是我的首选。3. 核心细节解析与实操要点理解了设计思路我们来看看具体怎么用。项目主要围绕几个核心概念展开函数定义、提示词生成、调用解析。3.1 函数定义不仅仅是类型提示定义一个能被llm-functions良好处理的函数你需要有意识地使用类型注解和文档字符串。from typing import Literal from pydantic import BaseModel, Field # 示例1基础函数 def search_web(query: str, max_results: int 5) - list[str]: 使用搜索引擎进行网页搜索。 Args: query: 搜索关键词应尽可能具体。 max_results: 返回的最大结果数量默认为5最多不超过10。 Returns: 一个包含搜索结果摘要的字符串列表。 # ... 实际的搜索逻辑 ... pass # 示例2使用Pydantic模型定义复杂参数 class BookingInfo(BaseModel): city: str Field(description目的地城市名称) check_in_date: str Field(description入住日期格式为YYYY-MM-DD) nights: int Field(gt0, le30, description入住晚数) room_type: Literal[standard, deluxe, suite] standard def book_hotel(booking: BookingInfo) - str: 根据提供的预订信息创建酒店订单。 # ... 实际的预订逻辑 ... pass实操要点与心得描述Description是关键虽然类型如str告诉了模型参数是字符串但description才真正告诉了模型这个字符串应该是什么。好的描述应简洁、无歧义并可能包含格式要求如日期格式或示例。llm-functions会优先使用Field中的description其次是文档字符串中的Args部分。善用Literal和Enum对于有限选项的参数一定要用Literal或Enum。这能极大减少模型的“胡思乱想”。比如unit: Literal[“celsius”, “fahrenheit”]比unit: str然后靠描述说明要好得多。复杂结构用Pydantic当参数本身是一个复杂对象如预订信息包含多个字段时将其定义为Pydantic的BaseModel是最佳实践。这不仅能生成清晰的Schema还能利用Pydantic进行后续的数据验证。返回值类型注解虽然LLM不直接执行返回的代码但清晰的返回类型如- dict或- list[str]有助于框架生成更完整的工具描述有时也能辅助后续处理。3.2 提示词生成与系统消息构建定义好函数后下一步就是生成能让LLM理解的指令。llm-functions提供了llm_functions装饰器或FunctionRegistry类来收集函数并生成系统消息。from llm_functions import llm_functions, FunctionRegistry # 方法1使用装饰器简洁适合函数数量少且固定的场景 llm_functions class MyTools: def get_weather(self, city: str) - str: ... def send_email(self, to: str, subject: str, body: str) - bool: ... # 生成系统消息 tools MyTools() system_message tools.to_system_message() # 输出是一个字符串包含了所有函数的描述和调用格式说明 # 方法2使用FunctionRegistry灵活适合动态注册函数 registry FunctionRegistry() registry.register(search_web) registry.register(book_hotel) system_message registry.to_system_message() # 或者生成OpenAI格式的工具描述列表 openai_tools registry.to_openai_tools()生成的system_message可以直接作为对话历史中的第一条系统消息发送给LLM。openai_tools则是一个列表可以直接用于OpenAI API的tools参数。注意事项消息长度如果你注册了很多函数生成的系统消息可能会非常长消耗大量Tokens。在实际应用中需要根据场景精选必要的函数或者考虑分拆成多个具有不同职能的智能体。模型兼容性对于非OpenAI的模型to_system_message()生成的纯文本指令通用性更好。你需要测试你的目标模型是否能很好地理解这种格式。有时可能需要手动微调一下生成的提示词模板。函数名和参数名的可读性使用清晰、英文小写加下划线的函数名和参数名如calculate_monthly_revenue这本身就能被LLM很好地理解减少对描述的依赖。3.3 调用解析与响应处理LLM在理解了工具后会在回复中尝试以特定格式通常是JSON发起函数调用。你的应用需要解析这个回复执行对应的函数并将结果返回给LLM让它生成最终面向用户的回答。import json from llm_functions import FunctionRegistry # 假设我们已经有了registry和定义好的函数 registry FunctionRegistry() registry.register(get_weather) # 模拟LLM的回复在实际中这是你从API收到的响应 llm_response_text 我需要查询天气。我将调用get_weather函数。 json {function: get_weather, arguments: {city: 北京}}1. 解析LLM回复提取函数调用请求这里需要一个简单的解析器来从回复文本中提取JSON块。llm-functions可能提供或推荐一些辅助解析方法。import re json_match re.search(rjson\n(.*?)\n, llm_response_text, re.DOTALL) if json_match: call_request json.loads(json_match.group(1)) func_name call_request.get(function) func_args call_request.get(arguments, {})# 2. 从注册表中获取函数并调用 func registry.get_function(func_name) if func: try: # 执行实际函数 result func(**func_args) # 将结果格式化为LLM可继续处理的文本 execution_result f函数 {func_name} 执行成功返回结果{result} except Exception as e: execution_result f函数 {func_name} 执行失败错误{str(e)} else: execution_result f未知函数{func_name}else: # LLM没有调用函数是普通回复 execution_result None3. 将执行结果作为新的上下文再次发送给LLM让它生成最终回答。完整的对话历史可能是[系统消息, 用户问题, LLM第一次回复(含函数调用), 函数执行结果]然后请求LLM基于所有历史生成最终答案。**核心环节与避坑技巧** * **解析的鲁棒性**LLM的回复格式不一定完全标准。它可能把JSON放在代码块里也可能直接输出JSON字符串甚至可能在JSON前后加一些解释性文字。你需要编写一个健壮的解析器比如用正则表达式匹配{}或json块并做好错误处理。一些更完善的框架或llm-functions的高阶用法可能会封装这部分逻辑。 * **参数验证与转换**即使LLM理解了Schema它提供的参数值也可能不符合要求比如字符串传入数字。**强烈建议在执行函数前用Pydantic模型对arguments字典进行验证和解析**。这能提前捕获类型错误避免函数内部报出难以理解的异常。 python # 假设函数已用Pydantic模型定义参数 class WeatherParams(BaseModel): city: str unit: Literal[celsius, fahrenheit] celsius # 在调用函数前 validated_args WeatherParams(**func_args).dict() result get_weather(**validated_args) * **错误处理与用户反馈**函数执行可能失败网络超时、API限流、参数无效。不要把原始的异常堆栈直接扔给LLM或用户。应该捕获异常生成一个对用户或LLM友好的错误信息如“查询天气服务暂时不可用请稍后再试”并将其作为函数执行结果返回给LLM让它组织语言告知用户。 * **上下文管理**这是一个典型的多轮对话流程。你需要维护一个对话历史列表每次都将最新的用户输入、LLM回复、函数执行结果追加进去并在下一次请求时完整发送。注意Token数量的限制对于长对话可能需要做摘要或选择性遗忘。 ## 4. 实战构建一个多功能查询助手 让我们通过一个更完整的例子串联起所有环节。我们将构建一个简单的智能助手它可以查询天气和解释名词。 ### 4.1 定义工具函数 首先我们定义两个工具函数。为了模拟真实环境我们使用假的实现。 python import random from typing import List from pydantic import BaseModel, Field from datetime import date # 工具1查询天气 def get_weather(city: str, date_str: str None) - str: 获取指定城市未来三天的天气预报。 Args: city: 城市名称例如“上海”、“New York”。 date_str: 查询日期格式YYYY-MM-DD。默认为今天。 # 模拟数据 forecasts [晴, 多云, 小雨, 阴, 雷阵雨] if date_str: query_date date_str else: query_date str(date.today()) # 模拟返回三天的天气 result [f{query_date}今天: {random.choice(forecasts)}] for i in range(1, 3): future_date f未来第{i}天 result.append(f{future_date}: {random.choice(forecasts)}) return f{city}的天气预报\n \n.join(result) # 工具2解释名词使用Pydantic模型 class ExplanationRequest(BaseModel): term: str Field(description需要解释的专业术语或名词) audience: str Field( description解释的目标受众, defaultgeneral, regex^(general|student|expert)$ ) def explain_term(request: ExplanationRequest) - str: 向不同受众解释一个专业术语。 explanations { general: f‘{request.term}’是一个专业概念简单来说它指的是...通俗解释, student: f‘{request.term}’在学科中的定义是...它的主要特点是...教学式解释, expert: f关于‘{request.term}’其最新研究进展包括...当前面临的挑战是...深入探讨, } return explanations.get(request.audience, 未知受众类型。)4.2 创建函数注册表并生成提示from llm_functions import FunctionRegistry # 创建注册表并注册函数 registry FunctionRegistry() registry.register(get_weather) registry.register(explain_term) # 注意这里注册的是函数本身框架会自动处理其参数模型 # 生成系统消息 system_prompt registry.to_system_message() print( 生成的系统提示部分) print(system_prompt[:500]) # 打印前500字符预览生成的system_prompt会是一段详细的文本告诉LLM“你是一个助手可以调用以下工具1.get_weather工具用于...调用格式是...2.explain_term工具用于...调用格式是...”4.3 模拟与LLM的交互循环这里我们模拟一个简化版的交互流程。在实际中你需要连接真实的LLM API。import json import re class SimpleLLMAssistant: def __init__(self, registry): self.registry registry self.conversation_history [ {role: system, content: registry.to_system_message()} ] def _parse_function_call(self, llm_text: str): 一个简单的解析器从LLM回复中提取JSON函数调用。 # 尝试匹配 json ... 代码块 pattern rjson\s*(.*?)\s* match re.search(pattern, llm_text, re.DOTALL) if match: try: return json.loads(match.group(1)) except json.JSONDecodeError: pass # 如果没有代码块尝试直接找最外层的 {...} # 这是一个更简单但可能不稳定的方法 try: start llm_text.find({) end llm_text.rfind(}) 1 if start ! -1 and end ! 0: return json.loads(llm_text[start:end]) except: pass return None def chat_round(self, user_input: str): 处理一轮用户输入。 # 1. 将用户输入加入历史 self.conversation_history.append({role: user, content: user_input}) # 2. 模拟调用LLM这里用假响应代替真实API调用 # 在实际中这里是将self.conversation_history发送给GPT/Claude等API print(f\n[用户] {user_input}) # 为了演示我们根据用户输入硬编码一个“智能”的LLM回复 llm_response if 天气 in user_input: llm_response 用户想查询天气。我需要调用get_weather函数。 json {function: get_weather, arguments: {city: 上海}} elif 解释 in user_input or 什么是 in user_input: llm_response 用户需要解释一个术语。我需要调用explain_term函数。{function: explain_term, arguments: {term: 机器学习, audience: general}} else: llm_response 我目前只能帮您查询天气或解释名词请告诉我您需要哪方面的帮助print(f[LLM原始回复]\n{llm_response}) self.conversation_history.append({role: assistant, content: llm_response}) # 3. 解析LLM回复看是否包含函数调用 call_request self._parse_function_call(llm_response) if call_request: func_name call_request.get(function) func_args call_request.get(arguments, {}) print(f检测到函数调用请求: {func_name}, 参数: {func_args}) # 4. 执行函数 func self.registry.get_function(func_name) if func: try: # 对于使用Pydantic参数的函数需要特殊处理 if func_name explain_term: # explain_term 接受一个Pydantic模型实例 from pydantic import ValidationError try: req ExplanationRequest(**func_args) result func(req) except ValidationError as e: result f参数错误: {e} else: # get_weather 接受普通参数 result func(**func_args) execution_result f工具调用成功。结果{result} except Exception as e: execution_result f工具调用失败{str(e)} else: execution_result f未知工具{func_name} print(f[函数执行结果] {execution_result}) # 将执行结果加入历史让LLM生成最终回答 self.conversation_history.append({role: function, name: func_name, content: execution_result}) # 5. 再次“模拟”LLM基于结果生成最终回答 # 这里简单地将结果直接作为最终回复 final_reply f根据查询结果\n{execution_result.split(结果)[-1]} self.conversation_history.append({role: assistant, content: final_reply}) print(f[助手最终回复] {final_reply}) return final_reply else: # LLM没有调用函数直接返回其回复 print(f[助手回复] {llm_response}) return llm_response运行助手assistant SimpleLLMAssistant(registry) assistant.chat_round(今天上海天气怎么样) assistant.chat_round(请向普通人解释一下‘神经网络’)这个例子虽然简化了LLM调用部分但完整展示了从函数定义、提示生成、调用解析到执行的闭环。在实际项目中你需要将模拟LLM的部分替换为对真实API如OpenAI、Anthropic或本地模型的调用。 ## 5. 高级技巧与常见问题排查 在实际集成llm-functions到生产环境时你会遇到一些更具体的问题。这里分享一些经验和解决方案。 ### 5.1 处理复杂参数与嵌套结构 当你的函数参数是一个包含列表、嵌套对象的复杂结构时清晰的类型提示至关重要。 python from typing import List, Optional from pydantic import BaseModel, Field class Item(BaseModel): name: str Field(description商品名称) quantity: int Field(gt0, description购买数量) class OrderRequest(BaseModel): order_id: str Field(description订单号) items: List[Item] Field(description商品清单) shipping_address: Optional[str] Field(None, description收货地址如不填则使用默认地址) def place_order(request: OrderRequest) - str: 提交一个新订单。 # 业务逻辑 total_items sum(item.quantity for item in request.items) return f订单 {request.order_id} 已创建共 {total_items} 件商品。技巧对于嵌套的List[Item]llm-functions能很好地生成对应的JSON Schema告知LLMitems应该是一个对象数组每个对象有name和quantity字段。这比用自然语言描述“一个商品列表每个商品有名称和数量”要精确得多。5.2 动态函数注册与上下文感知有时你希望根据对话上下文或用户权限动态地提供不同的工具集。registry_basic FunctionRegistry() registry_basic.register(get_weather) registry_admin FunctionRegistry() registry_admin.register(get_weather) registry_admin.register(place_order) # ... 注册其他管理函数 def get_registry_for_user(user_role: str) - FunctionRegistry: if user_role admin: return registry_admin else: return registry_basic # 在每次会话开始时根据用户决定使用哪个注册表并生成对应的系统消息。5.3 常见问题与排查清单问题现象可能原因排查步骤与解决方案LLM完全不调用函数1. 系统提示词未正确发送或格式被破坏。2. 模型能力不足无法理解函数调用指令。3. 用户问题过于简单模型认为无需调用工具。1. 检查发送给API的system消息内容确保是完整的to_system_message()输出。2. 尝试使用能力更强的模型如GPT-4。对于开源模型检查其是否经过函数调用指令微调。3. 在系统提示词中更明确地要求模型“在需要时务必调用工具”。LLM调用了函数但参数错误1. 参数描述不清。2. 参数类型定义不严格如该用Literal却用了str。3. LLM的“幻觉”自行编造了不存在的参数。1. 为每个参数添加清晰、无歧义的description。2. 使用Literal,Enum或constr(regex...)严格限定取值范围或格式。3. 在解析后、执行前使用Pydantic进行强制验证和类型转换并给LLM反馈错误。解析JSON失败1. LLM的回复格式不符合预期如缺少代码块标记。2. JSON格式错误如缺少引号。1. 强化你的解析器支持多种格式如带或不带json标记。br2. 在系统提示词中明确要求LLM“将函数调用请求放在一个独立的json代码块中”。br3. 可以尝试使用LLM本身来修复格式或者使用json.loads的strict参数并做好异常捕获。函数执行结果未被LLM正确利用1. 返回给LLM的函数执行结果格式不佳。2. 对话历史管理混乱LLM丢失了上下文。1. 将函数结果格式化为清晰、简洁的文本。避免返回过长的原始数据或HTML。2. 确保将{role: function, name: ..., content: ...}这条消息正确地追加到对话历史中并随下一次请求发送。Token消耗过快1. 注册的函数过多导致系统提示词过长。2. 对话历史累积过长。1. 按需动态加载函数或对函数描述进行精简但需谨慎避免影响模型理解。2. 对长对话历史进行摘要Summarization或只保留最近N轮对话。5.4 性能与优化建议缓存提示词如果你的工具集是静态的不要每次请求都重新生成系统提示词。在应用启动时生成一次并缓存起来。异步执行如果函数调用涉及网络I/O如调用外部API务必使用异步函数async def并在异步框架如FastAPI、Sanic中执行避免阻塞整个应用。超时与重试为函数调用设置超时并考虑对可能失败的调用如网络请求实现重试机制。结构化结果虽然函数返回给LLM的是文本但你的内部处理可以也应该使用结构化数据。让函数返回字典或Pydantic模型对象然后在传递给LLM前序列化成易读的文本。这样既方便内部处理也便于LLM理解。sigoden/llm-functions这个项目就像给大模型装上了一套标准化的“操作手册”和“遥控器”。它通过代码这一精准的媒介极大地弥合了人类意图与机器执行之间的鸿沟。从我自己的使用体验来看它特别适合那些需要快速迭代、对模型兼容性有要求、或者希望保持技术栈轻量简洁的项目。当然它不是一个全能的Agent框架在复杂的流程编排、记忆管理等方面你可能还需要结合其他库。但无论如何在解决“让LLM可靠调用函数”这个核心问题上它提供了一种非常优雅且高效的方案。如果你正在为提示词调试而烦恼不妨试试它或许能帮你省下不少时间。