1. 项目概述当LLM学会“调用函数”如果你最近在折腾大语言模型LLM的应用开发特别是想让模型能稳定、可靠地执行一些结构化任务——比如从一段自由文本里提取出联系人信息、把用户模糊的需求翻译成数据库查询语句或者让模型帮你调用一个外部API——那你大概率已经体会过什么叫“玄学”了。你精心设计了一段提示词Prompt告诉模型“请把下面这段话里的姓名、电话和邮箱提取出来用JSON格式返回。” 模型大部分时候能给你一个漂亮的答案但偶尔它会突然“抽风”给你返回一段散文式的描述或者JSON的键名拼写错误甚至直接拒绝执行说“作为AI我不能…”。这种输出的不确定性和格式的随意性是LLM应用从“玩具”走向“生产环境”最大的拦路虎之一。sigoden/llm-functions这个项目就是为了解决这个核心痛点而生的。它不是一个新模型而是一个精巧的Python库。它的核心思想是将LLM的“自由发挥”约束到你预定义的“函数”框架里。你可以像写Python函数一样用类型注解Type Hints清晰地定义一个函数它叫什么名字需要什么参数每个参数是什么类型返回什么。然后把这个函数的“签名”交给LLm-functions库它就能自动生成高质量的提示词并引导LLM严格地按照这个格式来思考和输出。简单来说它让LLM从“一个富有创造力但难以预测的诗人”变成了“一个严格遵守接口规范的可靠工程师”。对于开发者而言这意味着可预测性、稳定性和极简的集成代码。我最初是在一个需要从海量客服对话中自动提取工单要素的项目中接触到它的当时我们被GPT-3.5输出格式的飘忽不定折磨得够呛手动写解析器异常痛苦。引入llm-functions后我们只需要定义好数据结构剩下的格式保证和解析工作它全包了开发效率提升了不止一个量级。2. 核心设计思路类型即契约llm-functions的设计哲学非常清晰它建立在两个关键认知之上第一LLM本质上理解自然语言和结构第二明确的约束能极大提升LLM输出的质量与稳定性。它的整个架构都围绕着“利用Python类型系统作为与LLM通信的契约”这一核心思想展开。2.1 从“提示词工程”到“函数定义”传统的方式是“提示词工程”。你需要绞尽脑汁地写“请以如下JSON格式回复{\”name\”: \”…\”, \”phone\”: \”…\”}。确保键名完全一致不要添加任何额外说明。” 这种方式有诸多问题提示词冗长、容易遗漏约束、格式复杂时难以描述、解析输出依然需要手动处理错误。llm-functions的思路是颠覆性的。它让你回到熟悉的编程范式定义函数。例如你想提取信息就定义一个extract_contact函数from typing import Optional from pydantic import BaseModel class ContactInfo(BaseModel): name: str phone: Optional[str] None email: Optional[str] None def extract_contact(text: str) - ContactInfo: 从文本中提取联系信息。 ...你不需要写具体的实现逻辑llm-functions会做以下事情分析函数签名读取函数名extract_contact、参数text及其类型str、返回值类型ContactInfo一个Pydantic模型。生成结构化提示自动将这些类型信息转化为LLM能理解的结构化描述比如“你必须返回一个符合ContactInfo JSON Schema的对象”。处理交互与解析将用户输入和这个提示组合发送给LLM。拿到LLM的回复后自动尝试将其解析成ContactInfo对象实例。错误处理与重试如果解析失败格式不对它可以自动调整提示或进行重试最终给你一个干净的Python对象或者抛出一个清晰的异常。这种方式把开发者的重心从“如何让LLM听懂人话”转移到了“如何定义清晰的数据结构”后者是我们更擅长、也更可靠的工作。2.2 关键技术栈Pydantic与类型注解的深度结合llm-functions的强大很大程度上得益于它站在了Pydantic和Python类型注解这两个“巨人”的肩膀上。Pydantic这是一个用于数据验证和设置管理的库通过Python类型注解来定义数据结构。它的BaseModel类功能极其强大支持丰富的字段类型str,int,List,Enum等、默认值、字段校验器。llm-functions直接使用Pydantic模型作为函数返回类型意味着LLM生成的内容会经过Pydantic的严格验证。如果LLM返回的phone字段包含非数字字符Pydantic在解析时就会报错llm-functions可以捕获这个错误并触发重试或反馈给用户。Python类型注解Type Hints这是llm-functions与LLM“对话”的语言基础。库内部会将复杂的类型如List[ContactInfo]、Dict[str, int]转换为LLM能理解的描述。例如Optional[str] None会被解释为“这个字段是字符串可以为空”。这种转换是自动且准确的远比手动用自然语言描述“可以留空”要可靠。一个重要的实操心得虽然llm-functions也支持使用typing.TypedDict或简单的Dict/List作为类型提示但我强烈推荐始终使用Pydantic的BaseModel。原因有三第一Pydantic提供了最强大、最直观的数据验证第二BaseModel的实例化对象使用起来非常方便点号访问属性第三llm-functions对BaseModel的支持最完善生成的提示词质量也最高。这算是从早期版本踩坑得来的一条铁律。2.3 支持的LLM后端不仅仅是OpenAI项目最初是为OpenAI的Chat Completion API设计的但它抽象了后端接口。现在通过额外的适配器它可以支持OpenAI最主流、最稳定的选择。Anthropic Claude通过litellm等桥梁库支持。本地模型任何提供了类OpenAI API的本地模型服务比如使用text-generation-webui或vLLM部署的Llama、Qwen等开源模型。这对于数据隐私要求高或需要控制成本的场景至关重要。Azure OpenAI企业级部署的常见选择。这种设计使得llm-functions成为一个LLM供应商中立的工具。你的业务逻辑函数定义与底层使用的LLM是解耦的。今天你可以用GPT-4追求极致效果明天为了成本可以换用Claude 3 Haiku后天因为合规要求切换到本地部署的Qwen你的核心代码几乎不需要改动。注意使用不同的LLM后端效果会有差异。功能越强、遵循指令能力越好的模型如GPT-4、Claude 3 Opus输出结果越稳定可靠。较小的开源模型可能在复杂格式上需要更精细的提示或多次重试。在实际项目中建议根据任务复杂度进行模型选型测试。3. 从零开始安装与基础用法拆解理论说得再多不如上手一试。我们从一个最简单的例子开始完整走一遍使用流程并拆解其中的每一个细节。3.1 环境准备与安装首先确保你的Python环境是3.8或以上版本。然后通过pip安装pip install llm-functions如果你计划使用Pydantic强烈建议也需要安装它pip install pydantic对于使用OpenAI后端你需要配置API密钥。最常见的方式是设置环境变量export OPENAI_API_KEY你的-api-key在Python脚本中你也可以在代码中设置import os os.environ[“OPENAI_API_KEY”] “你的-api-key”这里有个关键细节llm-functions默认使用gpt-3.5-turbo模型。如果你想使用gpt-4或其他模型需要在创建函数执行器时指定。考虑到成本与效果的平衡对于大多数信息提取、分类等确定性任务gpt-3.5-turbo已经足够好且经济。对于逻辑非常复杂或创造性要求高的函数再考虑升级。3.2 第一个函数文本分类器让我们实现一个简单的情绪分类函数。目标是输入一段用户评论输出positive积极、negative消极或neutral中性。from llm_functions import llm_function from enum import Enum # 步骤1用枚举定义清晰的类别 class Sentiment(Enum): POSITIVE “positive” NEGATIVE “negative” NEUTRAL “neutral” # 步骤2使用装饰器定义函数 llm_function def analyze_sentiment(text: str) - Sentiment: “””分析给定文本的情绪倾向。””” # 函数体留空llm-functions会处理。 # 步骤3像调用普通函数一样使用它 if __name__ “__main__”: result analyze_sentiment(“这款产品太棒了完全超出了我的预期”) print(result) # 输出: Sentiment.POSITIVE print(result.value) # 输出: ‘positive’ result2 analyze_sentiment(“等了三天才发货客服也找不到人失望。”) print(result2.value) # 输出: ‘negative’代码逐行解析与避坑指南使用Enum枚举这是定义有限、明确返回选项的最佳实践。LLM看到Sentiment枚举会明白只能从三个值中选一个极大减少了“胡言乱语”的可能。比在提示词里写“返回‘positive’、‘negative’或‘neutral’”要严谨得多。llm_function装饰器这是核心魔法。它修饰了analyze_sentiment函数将其注册为一个由LLM驱动的函数。装饰器会分析函数的签名和文档字符串。文档字符串Docstring“””分析给定文本的情绪倾向。”””这非常重要这个描述会被整合进提示词帮助LLM理解这个函数的意图。写清楚、写准确能显著提升效果。调用方式analyze_sentiment(“…”)。看起来和调用一个本地函数一模一样但内部发生了网络请求、提示词构建、LLM推理和结果解析等一系列复杂操作。这种抽象极大地简化了开发者的心智负担。第一次运行可能会遇到的问题网络超时如果API请求慢或失败默认可能会有超时。你可以通过配置llm_functions.config来设置超时时间。API密钥错误确保OPENAI_API_KEY环境变量已正确设置并且有足够的额度。输出不是枚举值极低概率下LLM可能返回“POSITIVE”全大写或“积极”中文。这是因为枚举值定义的是小写但LLM的训练数据中可能有大写形式。解决方法是在枚举值的描述上更明确或者使用try-except捕获解析异常让llm-functions自动重试。3.3 进阶示例复杂信息提取与嵌套对象现在我们来处理一个更真实的场景从一段非结构化的订单通知文本中提取出结构化的订单信息。from llm_functions import llm_function from pydantic import BaseModel, Field from typing import List, Optional from datetime import date # 步骤1用Pydantic定义复杂的数据结构 class OrderItem(BaseModel): product_name: str Field(description“商品名称”) quantity: int Field(gt0, description“商品数量必须大于0”) unit_price: float Field(ge0, description“商品单价”) class ShippingAddress(BaseModel): recipient: str phone: str city: str district: str detail: str class OrderInfo(BaseModel): order_id: Optional[str] Field(None, description“订单号可能没有”) order_date: Optional[date] None total_amount: float items: List[OrderItem] address: ShippingAddress notes: Optional[str] None # 步骤2定义提取函数 llm_function def extract_order_info(text: str) - OrderInfo: “””从文本中提取订单信息。文本可能是客服对话、邮件或通知。””” # 步骤3使用 if __name__ “__main__”: order_text “”” 用户咨询我昨天2023-10-26下的单订单号好像是DD202310261234。 买了2本《Python编程从入门到实践》单价89.9元还有1个无线鼠标单价199元。 寄到张三13800138000北京市海淀区中关村大街1号。总额是378.8元吧麻烦尽快发货。 “”” order extract_order_info(order_text) print(f“订单号: {order.order_id}”) # DD202310261234 print(f“总金额: {order.total_amount}”) # 378.8 print(f“商品数: {len(order.items)}”) # 2 for item in order.items: print(f“ - {item.product_name} x {item.quantity}”) print(f“收货人: {order.address.recipient}”) # 张三这个例子揭示了llm-functions更强大的能力嵌套模型OrderInfo包含了OrderItem列表和ShippingAddress对象。llm-functions能够处理这种嵌套结构并生成相应的复杂JSON Schema提示给LLM。字段描述与约束Field(description“…”提供了字段的语义描述这会被直接用于提示词帮助LLM准确理解每个字段的含义。gt0大于0、ge0大于等于0是Pydantic的验证器它们主要作用于后端解析验证但LLM在理解“数量”时也能从上下文学习到这是一个正数。日期等特殊类型date类型会被自动处理。LLM需要识别文本中的“2023-10-26”并格式化为标准的日期字符串。可选字段Optional[str] None明确告诉LLM和解析器这个字段可能不存在。这在实际场景中非常关键因为源文本可能不包含订单号。实操心得如何设计一个好的Pydantic模型命名清晰字段名最好用英文且能自解释。product_name比name好因为后者在地址里也可能出现。善用description对于容易混淆的字段一定要加描述。例如total_amount的描述可以写“订单含税总价”避免LLM理解为 subtotal小计。区分“业务约束”和“格式约束”像gt0这样的约束是业务逻辑。LLM可能会偶然违反比如输出0这时Pydantic会在解析时报错llm-functions可以重试。但对于“手机号必须是11位数字”这种强格式约束仅靠LLM可能不可靠。更稳妥的做法是让LLM先提取出原始文本如“13800138000”然后在后续的业务代码中再用正则表达式或专门的库进行清洗和验证。不要过分依赖LLM做精确的格式校验。4. 高级功能与实战配置掌握了基础用法后我们需要深入了解如何配置和优化llm-functions以应对生产环境中的各种需求。4.1 模型、温度与重试策略默认配置可能不适合所有场景。llm_function装饰器支持多种参数来定制行为from llm_functions import llm_function, config import openai # 方法1通过装饰器参数配置单个函数 llm_function( model“gpt-4”, # 指定使用GPT-4 temperature0.1, # 降低创造性提高确定性 max_retries2, # 解析失败时重试2次 timeout30.0, # 请求超时30秒 ) def precise_extraction(text: str) - MyComplexModel: “””这是一个需要高精度和稳定性的提取任务。””” # 方法2通过全局config配置 config.default_model “gpt-4-turbo-preview” config.default_temperature 0.2 config.default_max_retries 3 config.openai_client openai.OpenAI(api_key“your-key”, timeout60.0) # 自定义OpenAI客户端 # 此后创建的所有llm_function如无特别指定都会使用上述默认配置 llm_function # 这个函数将使用gpt-4-turbo-previewtemperature0.2 def another_function() - str: “””…”””关键参数解读model最重要的配置。对于格式化任务gpt-3.5-turbo性价比高gpt-4系列更可靠但成本也高gpt-4-turbo在长上下文和精度上平衡较好。务必根据任务测试选择。temperature控制随机性。对于函数调用强烈建议设置为0到0.3之间。越接近0输出越确定、可重复。设置为0有时会导致输出僵化0.1或0.2是个不错的起点。max_retries当LLM返回的内容无法被解析成目标类型时库会自动重试。重试时会附带之前的错误信息帮助LLM纠正。设置2-3次重试能显著提高成功率但会增加延迟和成本。timeout网络或模型响应慢时的保险丝。生产环境建议设置一个合理的超时如30秒并做好超时异常处理。4.2 流式输出与异步支持对于需要长时间运行或希望实现“打字机”效果的应用llm-functions支持流式输出和异步调用。异步调用import asyncio from llm_functions import llm_function_async # 注意是异步装饰器 llm_function_async async def async_extract(text: str) - MyModel: “””异步提取函数。””” async def main(): results await asyncio.gather( async_extract(text1), async_extract(text2), async_extract(text3), ) # 并行处理多个请求极大提升吞吐量 asyncio.run(main())在Web后端如FastAPI中使用异步函数可以避免阻塞事件循环高效处理并发请求。流式输出部分后端支持 流式输出主要适用于LLM生成长文本的场景。对于函数调用LLM通常是生成一个完整的JSON对象流式意义不大。但如果你定义的函数返回类型是str例如一个总结函数并且希望看到生成过程可以探索相关配置。不过核心的格式化提取功能一般不需要流式。4.3 函数组合与复杂工作流llm-functions的真正威力在于可以将多个LLM函数像乐高积木一样组合起来构建复杂的工作流。假设我们有一个电商评论分析流水线提取实体从评论中提取产品名、品牌、属性。判断情感对评论整体进行情感分析。总结要点如果情感为负面则总结用户抱怨的核心问题。from llm_functions import llm_function from pydantic import BaseModel from typing import List class ProductMention(BaseModel): name: str brand: Optional[str] attributes: List[str] class Sentiment(Enum): POSITIVE “positive” NEGATIVE “negative” NEUTRAL “neutral” class ComplaintSummary(BaseModel): main_issue: str severity: int # 1-5 llm_function def extract_products(text: str) - List[ProductMention]: “””提取评论中提到的产品信息。””” llm_function def analyze_sentiment(text: str) - Sentiment: “””分析评论情感。””” llm_function def summarize_complaint(text: str) - ComplaintSummary: “””总结负面评论的核心投诉点。””” def analyze_review_pipeline(review_text: str): “””组合多个LLM函数的流水线。””” # 并行或串行执行 products extract_products(review_text) sentiment analyze_sentiment(review_text) result { “products”: products, “sentiment”: sentiment, } if sentiment Sentiment.NEGATIVE: # 只有负面评论才调用总结函数 summary summarize_complaint(review_text) result[“complaint_summary”] summary return result这种模式的优点模块化每个函数职责单一易于测试和调试。可复用extract_products函数可以用在商品库构建、竞品分析等多个场景。条件逻辑可以在Python代码中轻松加入if-else、循环等业务逻辑控制LLM函数的调用流程。降级与兜底如果某个LLM函数调用失败或超时你可以在管道中捕获异常返回默认值或调用一个更简单、更便宜的模型版本实现优雅降级。5. 生产环境部署避坑指南与性能优化将基于llm-functions的应用部署到生产环境会面临与本地开发截然不同的挑战。以下是我在多个项目中总结出的核心经验和避坑指南。5.1 错误处理与鲁棒性设计LLM API调用可能因为网络、速率限制、模型过载、内容策略等多种原因失败。llm-functions本身会抛出一些异常但我们需要构建更健壮的系统。from llm_functions import llm_function, LLMFunctionError import openai from tenacity import retry, stop_after_attempt, wait_exponential # 示例一个带有重试和降级策略的稳健函数 llm_function def robust_extraction(text: str) - Optional[MyModel]: “””稳健的信息提取可能返回None。””” # 使用tenacity库进行更精细的重试控制处理网络抖动、速率限制 retry( stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10), retryretry_if_exception_type((openai.APITimeoutError, openai.RateLimitError)) ) def call_llm_with_retry(text): try: return robust_extraction(text) except LLMFunctionError as e: # llm-functions解析错误可能是模型输出格式不对 log.warning(f“LLM解析失败: {e}”) return None except openai.APIConnectionError: log.error(“网络连接错误”) raise # 触发tenacity重试 except openai.RateLimitError: log.warning(“触发速率限制等待后重试”) raise # 触发tenacity重试 except Exception as e: # 其他未知错误 log.exception(“未知错误发生在LLM调用中”) return None # 在业务逻辑中 def process_document(doc): result call_llm_with_retry(doc) if result is None: # 降级策略使用规则引擎或更简单的方法 result fallback_extraction(doc) return result关键点区分错误类型网络错误APIConnectionError,APITimeoutError通常值得重试。速率限制错误RateLimitError需要指数退避重试。内容策略错误如InvalidRequestError可能提示你的输入或函数定义有问题重试可能无效。llm-functions解析错误LLMFunctionError通常意味着模型没有返回有效格式。可以记录日志并考虑是否重试max_retries已配置、使用更详细的提示或切换到更强大的模型。设置降级方案当LLM完全不可用或持续失败时必须有备用方案。可以是基于规则的正则表达式提取可以是返回一个默认值也可以是标记该任务需要人工处理。5.2 性能、成本与缓存策略LLM API调用通常是应用中最耗时的环节也是成本的主要来源。1. 批量处理 避免在循环中逐条调用API。尽可能将输入数据批量发送。虽然llm-functions本身是函数式调用但你可以通过组织输入数据来实现“逻辑批量”。from concurrent.futures import ThreadPoolExecutor def batch_extract(texts: List[str]) - List[Optional[MyModel]]: “””使用线程池并发处理多个文本。””” with ThreadPoolExecutor(max_workers5) as executor: # 控制并发数避免触发速率限制 futures [executor.submit(robust_extraction, text) for text in texts] results [f.result() for f in futures] return results注意并发数max_workers需要根据你的API速率限制RPM, TPM谨慎设置。2. 缓存 对于相对静态或重复的查询缓存结果可以大幅提升响应速度并降低成本。例如处理相似的客服问题时很多用户问法不同但核心信息相同。from functools import lru_cache import hashlib lru_cache(maxsize1000) def cached_extraction(text: str) - Optional[MyModel]: “””对提取结果进行缓存。注意需要确保text完全相同才会命中缓存。””” # 可以对text进行一些标准化预处理比如去除多余空格、转换为小写以增加缓存命中率 normalized_text text.strip().lower() # 但要注意标准化可能改变语义需根据业务判断 return robust_extraction(normalized_text)更复杂的缓存可以考虑使用Redis等外部缓存并设置合理的TTL生存时间。3. 成本监控 OpenAI等API按Token收费。你需要估算和监控使用量。估算你的提示词包含自动生成的函数描述和用户输入的总长度加上模型返回的长度。llm-functions生成的提示词通常比较长特别是对于复杂模型。监控在调用前后记录Token使用量OpenAI响应头中包含usage字段并汇总到你的监控系统如Prometheus。设置告警阈值。5.3 安全与内容审核直接将用户输入传递给LLM存在风险提示词注入、生成有害内容。虽然llm-functions主要用于结构化输出风险较低但仍需注意。输入过滤与清理在调用LLM函数前对用户输入进行基本的清理和检查比如过滤过长的文本、检查是否包含明显的恶意代码或敏感词。输出验证即使LLM返回了结构正确的Pydantic对象也要对内容进行业务逻辑验证。例如提取出的电话号码是否符合国家规范金额是否在合理范围内使用安全模型OpenAI等提供商通常有内容安全层但你不能完全依赖。对于高风险场景可以考虑在调用LLM前后加入你自己或第三方的文本内容审核API。6. 典型应用场景与案例剖析llm-functions的适用场景非常广泛任何需要将非结构化文本转换为结构化数据的任务都是它的用武之地。下面通过几个典型案例看看它如何解决实际问题。6.1 场景一智能客服工单自动创建痛点用户通过在线聊天、邮件或语音转文本提交问题描述千奇百怪。客服人员需要手动阅读判断问题类型售后、技术、投诉提取关键信息订单号、产品型号、问题描述再录入工单系统。效率低易出错。解决方案from enum import Enum from pydantic import BaseModel, Field from typing import List class TicketCategory(Enum): AFTER_SALES “after_sales” TECHNICAL “technical” COMPLAINT “complaint” BILLING “billing” class CustomerTicket(BaseModel): category: TicketCategory order_id: Optional[str] Field(None, description“相关订单号”) product_sku: Optional[str] Field(None, description“产品SKU或型号”) issue_summary: str Field(description“用户问题的核心摘要”) urgency: int Field(ge1, le5, description“紧急程度1最低5最高”) tags: List[str] Field(default_factorylist, description“问题标签如’无法开机’’退款’等”) llm_function(model“gpt-4”, temperature0.1) def create_ticket_from_text(user_input: str) - CustomerTicket: “””根据用户输入的自然语言描述自动创建结构化工单。””” # 集成到工单系统 def on_new_customer_message(message: str): try: ticket_data create_ticket_from_text(message) # 将ticket_data一个Pydantic对象直接传入工单系统的创建API ticket_id ticket_system_api.create_ticket(**ticket_data.dict()) # 甚至可以自动回复“您的问题已记录工单号{ticket_id}…” except Exception as e: # 如果自动创建失败转入人工处理队列 assign_to_human_agent(message)价值将客服人员从重复的信息提取和分类工作中解放出来工单创建速度从分钟级提升到秒级且数据格式统一便于后续分析和自动化处理。6.2 场景二市场情报与竞品分析痛点市场团队需要从海量的新闻、社交媒体帖子、论坛讨论中追踪竞争对手的动态、产品发布、用户反馈。人工阅读和整理费时费力。解决方案class MarketIntel(BaseModel): company_name: str event_type: str Field(description“如’产品发布’’融资’’高管变动’’合作伙伴’等”) product_name: Optional[str] date_mentioned: Optional[date] key_points: List[str] sentiment: str # “positive”, “negative”, “neutral” llm_function def extract_intel_from_article(article_text: str) - List[MarketIntel]: “””从一篇长文中提取所有相关的市场情报片段。一篇文章可能提到多个公司多个事件。””” # 配合爬虫使用 def analyze_competitor_news(rss_feed_urls: List[str]): all_articles crawl_articles(rss_feed_urls) all_intel [] for article in all_articles: intel_list extract_intel_from_article(article.content) all_intel.extend(intel_list) # 将结果存入数据库或生成报告 save_to_database(all_intel) generate_weekly_report(all_intel)价值实现7x24小时无人值守的市场监测信息结构化后可直接导入数据库用于生成自动报告、趋势图表和预警通知。6.3 场景三内部文档知识库QA增强痛点公司内部有大量PDF、Word文档如产品手册、项目报告、会议纪要。员工查找信息困难只能靠记忆或全文搜索效率低下。解决方案结合RAG检索增强生成和llm-functions。检索阶段使用向量数据库检索与用户问题相关的文档片段。生成阶段将检索到的片段和用户问题一起交给LLM函数要求其生成结构化答案。class QAAnswer(BaseModel): answer: str Field(description“对问题的直接回答”) confidence: float Field(ge0, le1, description“答案的置信度”) source_documents: List[str] Field(description“引用来源的文档ID或片段”) follow_up_questions: List[str] Field(description“建议的后续追问问题”) llm_function def answer_with_sources(question: str, context: List[str]) - QAAnswer: “””基于提供的上下文回答问题并注明来源和置信度。””” def rag_qa_system(user_question: str): # 1. 检索相关文档片段 relevant_chunks vector_db.search(user_question, top_k5) # 2. 组合上下文 context_text “\n\n”.join([chunk.content for chunk in relevant_chunks]) # 3. 调用LLM函数获取结构化答案 answer answer_with_sources(questionuser_question, context[context_text]) return answer价值不仅提供了答案还提供了置信度和来源增加了可信度。结构化的输出follow_up_questions可以直接用于构建交互式对话界面提升用户体验。7. 常见问题排查与调试技巧即使有了llm-functions这样的利器在实际开发中依然会遇到各种问题。下面是一些常见问题的排查思路和调试技巧。7.1 LLM不遵循格式要求症状函数返回LLMFunctionError错误信息显示无法将LLM输出解析为指定的Pydantic模型。可能原因与解决方案问题原因排查步骤解决方案模型能力不足使用gpt-3.5-turbo处理非常复杂的嵌套对象或长列表。升级到gpt-4或gpt-4-turbo。对于简单任务可尝试调整提示。温度Temperature过高温度设置大于0.7导致输出随机性太强。将temperature设置为0.1或0.2这是解决格式问题最有效的方法之一。字段描述不清字段名过于简略如data或没有Field(description…)。为每个字段添加清晰、无歧义的description。用例子说明格式如Field(description”日期格式为YYYY-MM-DD”)。类型过于复杂使用了Union[TypeA, TypeB]或复杂的泛型。尽量避免Union。如果必须考虑拆分成两个函数。简化嵌套层级。提示词冲突函数文档字符串Docstring的指令与自动生成的格式指令冲突。检查并简化Docstring只描述函数“做什么”不要描述“返回什么格式”格式交给库来处理。调试技巧在llm_function装饰器中设置verboseTrue或者在调用前后打印出实际的请求和响应。llm-functions内部使用instructor库一个类似的优秀库你可以查看它发送给OpenAI的最终提示词是什么这有助于理解模型“看到”的指令。import logging logging.basicConfig(levellogging.DEBUG) # 设置日志级别为DEBUG有时能看到更多信息7.2 处理速度慢或超时症状函数调用耗时很长经常超时。排查与优化检查输入长度LLM的处理时间与输入Token数大致成正比。如果你的text参数是整篇长文档速度必然慢。考虑先对文档进行分块或摘要再将关键部分送入LLM函数。检查模型gpt-4比gpt-3.5-turbo慢得多。确认当前任务是否真的需要GPT-4的精度。网络延迟如果你的服务器和API服务器之间网络延迟高考虑使用同一区域的云服务。并发与速率限制过高的并发请求可能触发提供商的速率限制导致请求排队或延迟。调整你的并发控制策略。启用重试max_retries设置过高比如5次会导致单次失败请求的总耗时急剧增加。对于实时性要求高的场景设置为1或2并做好快速失败和降级处理。7.3 成本失控症状API账单增长过快。成本控制策略选择合适的模型用gpt-3.5-turbo完成大多数格式化任务。仅对最关键、最复杂的任务使用gpt-4。优化提示词间接通过设计更精简的Pydantic模型来优化。不必要的字段、过长的字段描述都会增加Token消耗。保持模型简洁。缓存如前所述对相同或相似的输入进行缓存。设置预算和告警在OpenAI控制台设置使用量预算和告警。考虑本地模型对于数据敏感或长期成本考量可以部署类似Qwen-7B-Chat、Llama-3-8B这样的开源模型并通过其兼容OpenAI的API接口与llm-functions对接。初期投入高但长期边际成本低。7.4 与现有代码库集成困难症状已有的业务代码是同步的但llm-functions的异步版本似乎更好用。解决方案在同步代码中调用异步函数可以使用asyncio.run()但要注意这会在当前线程创建新的事件循环。在Web框架如Flask中可能有问题。更通用的做法是使用asyncio.create_task然后在后台运行或者使用像anyio这样的库来桥接。坚持使用同步版本对于大多数应用同步的llm_function已经足够。除非你需要处理极高的并发每秒数百请求否则同步版本的简单性优势更大。抽象一层将LLM函数调用封装在一个单独的服务或模块中。业务代码通过一个简单的接口如HTTP或消息队列与这个模块通信。这样可以将异步/同步的复杂性隔离在模块内部。最后记住llm-functions是一个工具它的目标是让你更高效地利用LLM的能力而不是把你锁死。当任务简单到可以用正则表达式完美解决时就别用LLM。当规则极其复杂、变化频繁或者输入本身就是高度非结构化、充满歧义的自然语言时才是llm-functions大显身手的舞台。从一个小而具体的用例开始定义好清晰的数据结构逐步迭代和扩展你会发现自己构建AI应用的能力得到了质的飞跃。