ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Azure OpenAI 智能体开发:Assistants API、代码解释器与函数调用实战

Azure OpenAI 智能体开发:Assistants API、代码解释器与函数调用实战 1. 从补全对话到构建智能体Azure OpenAI 进阶能力全景拆解很多人在用 Azure OpenAI 的时候第一反应就是把它当成一个更稳定的聊天接口来用发一段提示词拿一段回复然后就结束了。但如果你只停在这一步其实只用了它不到三成的能力。真正让生成式 AI 从“玩具”变成“生产力工具”的是它背后那套围绕 Assistants API、代码解释器、函数调用构建起来的智能体框架。我接触过不少团队他们一开始也是拿 AOAI 做问答机器人后来发现业务里真正耗时的环节不是“回答问题”而是“回答问题之前要先查数据、算指标、调接口”这时候单纯的对话补全就顶不住了。这篇内容我打算把 Azure OpenAI 里几个容易被忽略但实战价值极高的能力拆开讲清楚。核心围绕三条线一是 Assistants API 怎么把对话、工具、文件、线程管理打包成一个可复用的智能体二是代码解释器在数据分析场景里到底能干什么、不能干什么三是函数调用怎么让模型安全地触达你自己的业务系统。这三块内容合在一起基本就构成了一个完整的“生成式 AI 应用骨架”。适合已经跑通过基础对话接口、想往业务系统里落地的开发者也适合产品经理用来判断哪些需求技术上可行、哪些是坑。我自己的经验是AOAI 的文档写得比较“正”但实际用起来有很多细节文档里不会强调比如线程的生命周期管理、运行状态轮询的超时处理、函数调用返回值的格式约束等等。这些细节不踩一遍坑是很难意识到的。下面我会按照“设计思路—核心细节—实操过程—问题排查”的顺序把每个能力讲透中间穿插我自己踩过的坑和总结出来的参数配置。2. Assistants API 整体设计与思路拆解2.1 为什么需要 Assistants API 而不是裸调 Chat Completions裸调 Chat Completions 的本质是“无状态”的你每次请求都要把完整的历史消息数组传进去模型不会记住上一轮说了什么。这在简单问答里没问题但一旦涉及多轮工具调用、文件检索、代码执行状态管理就会变得极其复杂。你需要自己维护消息历史、自己判断什么时候该调工具、自己处理工具返回结果再拼回上下文。Assistants API 的出现就是为了把这套“编排逻辑”从你的业务代码里抽出来交给平台托管。具体来说Assistants API 引入了四个核心对象Assistant助手、Thread线程、Message消息、Run运行。Assistant 定义了模型、指令、可用工具Thread 代表一段持续对话Message 是线程里的单条消息Run 是一次执行动作它会驱动模型去读取线程、决定是否调用工具、生成回复。这套抽象的好处是你不需要在每次请求里重复传系统提示词和工具定义也不需要手动拼接工具调用结果平台会帮你维护整个状态机。我实测下来对于需要多轮工具调用的场景用 Assistants API 比裸调 Chat Completions 的代码量能减少一半以上。但代价是你需要理解 Run 的状态流转否则很容易写出“一直轮询但永远不结束”的代码。2.2 线程与运行的生命周期管理逻辑Thread 的生命周期相对简单创建、追加消息、被 Run 消费、可以复用也可以删除。真正复杂的是 Run 的状态。一个 Run 从创建开始会经历queued、in_progress、requires_action、completed、failed、cancelled、expired等状态。其中requires_action是最关键的它表示模型决定要调用某个工具但需要你的代码去实际执行这个工具并把结果提交回来。很多人第一次写 Assistants API 的代码时会写一个while循环不断查询 Run 状态直到变成completed。这个逻辑本身没错但如果没有处理requires_action就会卡死。正确的做法是在轮询到requires_action时读取required_action.submit_tool_outputs.tool_calls逐个执行工具然后把结果通过submit_tool_outputs提交回去再继续轮询。注意Run 的状态轮询建议加指数退避初始间隔 500ms最大间隔 5s总超时建议设置在 120s 左右。我遇到过因为网络抖动导致 Run 长时间停在in_progress的情况没有超时保护的话请求会一直挂着。2.3 工具选型代码解释器、文件检索、函数调用怎么选Assistants API 目前支持三类工具代码解释器Code Interpreter、文件检索File Search、函数调用Function Calling。这三者不是互斥的一个 Assistant 可以同时挂多个工具。但实际用的时候要有取舍因为工具越多模型决策的复杂度越高出错概率也越大。代码解释器适合需要执行 Python 代码的场景比如数据统计、图表生成、文件格式转换。它的优势是沙箱环境由平台提供你不需要自己搭执行环境劣势是执行时间有限制而且不能访问外部网络。文件检索适合知识库问答你上传文件后平台会自动做向量化模型在回答时会先检索相关片段。函数调用适合需要触达外部系统的场景比如查订单、发邮件、调内部 API模型只负责决定“调哪个函数、传什么参数”实际执行由你的代码完成。我的建议是如果一个 Assistant 同时需要代码解释器和函数调用优先把数据处理逻辑放在代码解释器里把外部系统交互放在函数调用里不要让两者职责重叠。否则模型可能会在“用代码算”和“调函数查”之间反复横跳浪费 token 还容易出错。3. 代码解释器核心细节与实操要点3.1 代码解释器的运行机制与能力边界代码解释器本质上是一个托管的 Python 沙箱模型可以在里面写代码、执行代码、读取执行结果然后基于结果继续推理。它支持大部分常用库包括 pandas、numpy、matplotlib、openpyxl 等。你上传的 CSV、Excel、JSON 文件会被挂载到沙箱里模型可以直接用代码读取。但它的边界也很明确。第一不能访问外部网络所以任何需要调 API 的操作都做不了。第二单次执行有时间限制复杂计算可能会超时。第三沙箱是无状态的每次 Run 之间文件系统不共享如果你需要跨 Run 保留中间结果得把结果写回消息或者上传成新文件。第四生成的图表会以文件形式返回你需要通过文件下载接口去取。我踩过的一个坑是上传了一个 50MB 的 CSV让模型做分组聚合结果 Run 跑了很久最后失败了。后来发现是文件太大导致沙箱加载慢加上模型生成的代码没有做分块处理。解决办法是提前在本地把数据裁剪到必要列和必要行或者用文件检索先做筛选再交给代码解释器。3.2 文件上传与挂载的实操细节使用代码解释器之前需要先把文件上传到 AOAI 的文件接口拿到 file_id然后在创建 Assistant 或 Message 时把 file_id 挂上去。这里有个细节文件可以挂在 Assistant 级别也可以挂在 Message 级别。挂在 Assistant 级别表示这个文件对所有使用该 Assistant 的线程都可见挂在 Message 级别表示只对当前这条消息可见。实际操作中我建议把“长期知识库”类文件挂在 Assistant 级别把“本次对话临时数据”挂在 Message 级别。这样既避免了重复上传又不会让临时数据污染其他对话。# 上传文件示例 from openai import AzureOpenAI client AzureOpenAI( azure_endpointhttps://your-resource.openai.azure.com/, api_keyyour-api-key, api_version2024-05-01-preview ) # 上传文件 file client.files.create( fileopen(sales_data.csv, rb), purposeassistants ) print(file.id) # 创建带代码解释器的 Assistant assistant client.beta.assistants.create( name数据分析助手, instructions你是一个数据分析助手使用代码解释器分析用户上传的数据。, modelgpt-4o, tools[{type: code_interpreter}], tool_resources{ code_interpreter: { file_ids: [file.id] } } )提示文件上传后有一个处理时间刚上传完立刻创建 Run 可能会报“文件未就绪”。建议上传后 sleep 1-2 秒再使用或者捕获异常后重试。3.3 代码解释器返回结果的处理技巧代码解释器执行完代码后结果会以两种形式返回一种是文本输出比如 print 的结果一种是文件输出比如生成的图表、导出的 Excel。文本输出会直接出现在 Message 的 content 里文件输出会出现在 Message 的 attachments 里。处理文件输出时你需要用client.files.content(file_id)去下载内容。这里有个容易忽略的点文件下载链接是有有效期的而且返回的是二进制流需要自己保存成文件。我一般会在代码里封装一个download_attachments函数遍历 Message 的 attachments逐个下载并保存到本地目录。另外模型生成的代码有时候会包含plt.show()这种在沙箱里无效的调用导致图表不显示。解决办法是在 instructions 里明确要求“生成图表时使用 plt.savefig 保存为文件不要调用 plt.show()”。这个细节文档里不会写但不加的话图表生成成功率会明显下降。4. 函数调用核心细节与实操要点4.1 函数调用的决策流程与参数约束函数调用的核心逻辑是你在创建 Assistant 时提供一组函数定义JSON Schema 格式模型在对话过程中判断是否需要调用某个函数如果需要它会返回函数名和参数你的代码执行函数后把结果提交回去模型再基于结果生成最终回复。这里最关键的是函数定义的参数约束。JSON Schema 支持类型、枚举、必填项、描述等字段。描述字段尤其重要因为模型是根据描述来判断什么时候该调这个函数的。我见过很多函数调用失败的案例根源都是描述写得太模糊比如“查询数据”这种描述模型根本不知道查什么数据、什么时候该查。一个好的函数描述应该包含这个函数做什么、什么场景下使用、参数的含义和格式。比如“根据订单号查询订单状态当用户询问订单进度时使用。参数 order_id 为字符串格式的订单编号例如 ORD-2024-001”。4.2 函数返回值的格式与错误处理函数执行完后的返回值必须是字符串。如果你返回的是 JSON 对象需要先序列化成字符串。返回值的内容会作为工具输出提交给模型模型会基于这个内容继续推理。错误处理是函数调用里最容易出问题的地方。如果函数执行失败你有两种选择一是返回一个描述错误的字符串让模型知道调用失败了二是直接抛出异常中断整个 Run。我的建议是返回错误描述字符串因为模型有时候能根据错误信息调整参数重试直接中断反而失去了自愈机会。但要注意错误描述不要暴露敏感信息比如数据库连接字符串、内部 IP 等。我一般会返回“查询失败请检查订单号是否正确”这种通用描述同时在服务端记录详细日志。4.3 多函数并行调用的处理策略当模型决定同时调用多个函数时tool_calls数组里会有多个条目。你需要逐个执行然后把所有结果一起提交回去。提交时的tool_outputs数组顺序要和tool_calls的顺序一致每个条目包含tool_call_id和output。这里有个坑如果其中一个函数执行时间很长会阻塞其他函数的执行。我的做法是用并发的方式执行多个函数然后等所有结果都拿到后再一起提交。但要注意并发数不要太高避免把下游系统打挂。import concurrent.futures def handle_tool_calls(tool_calls): results [] with concurrent.futures.ThreadPoolExecutor(max_workers5) as executor: future_to_call { executor.submit(execute_function, call): call for call in tool_calls } for future in concurrent.futures.as_completed(future_to_call): call future_to_call[future] try: output future.result() except Exception as e: output f函数执行失败: {str(e)} results.append({ tool_call_id: call.id, output: output }) return results注意提交 tool_outputs 时如果某个 tool_call_id 对应的 output 为空字符串模型可能会认为该函数没有返回结果。建议至少返回“执行成功无返回数据”这样的占位描述。5. 完整实操流程与关键环节实现5.1 环境准备与依赖配置在开始写代码之前需要确认几件事Azure OpenAI 资源已经创建并且部署了支持 Assistants API 的模型目前主要是 gpt-4o、gpt-4-turbo 等API 版本使用2024-05-01-preview或更高Python 环境安装了openai库版本建议 1.30 以上。pip install openai1.30.0环境变量建议这样配置避免把密钥硬编码在代码里export AZURE_OPENAI_ENDPOINThttps://your-resource.openai.azure.com/ export AZURE_OPENAI_API_KEYyour-api-key export AZURE_OPENAI_API_VERSION2024-05-01-preview export AZURE_OPENAI_DEPLOYMENTgpt-4o我习惯把 deployment 名称也做成环境变量因为不同环境开发、测试、生产可能用不同的部署名硬编码会导致切换环境时改代码。5.2 创建 Assistant 与 Thread 的完整代码下面是一个完整的初始化流程包含 Assistant 创建、Thread 创建、消息追加。import os from openai import AzureOpenAI client AzureOpenAI( azure_endpointos.getenv(AZURE_OPENAI_ENDPOINT), api_keyos.getenv(AZURE_OPENAI_API_KEY), api_versionos.getenv(AZURE_OPENAI_API_VERSION) ) # 定义函数调用工具 functions [ { type: function, function: { name: query_order_status, description: 根据订单号查询订单状态当用户询问订单进度时使用。, parameters: { type: object, properties: { order_id: { type: string, description: 订单编号格式如 ORD-2024-001 } }, required: [order_id] } } } ] # 创建 Assistant assistant client.beta.assistants.create( name订单助手, instructions你是一个订单查询助手用户询问订单状态时调用 query_order_status 函数。, modelos.getenv(AZURE_OPENAI_DEPLOYMENT), toolsfunctions ) # 创建 Thread thread client.beta.threads.create() # 追加用户消息 client.beta.threads.messages.create( thread_idthread.id, roleuser, content帮我查一下订单 ORD-2024-001 的状态 )这段代码跑通后你会得到一个 assistant_id 和 thread_id后续的 Run 操作都基于这两个 ID。5.3 Run 执行与工具调用回传的完整实现这是整个流程里最核心的部分。下面是一个完整的 Run 执行循环包含状态轮询、工具调用处理、结果提交。import time def run_assistant(thread_id, assistant_id): run client.beta.threads.runs.create( thread_idthread_id, assistant_idassistant_id ) max_wait 120 start_time time.time() interval 0.5 while run.status in [queued, in_progress, requires_action]: if time.time() - start_time max_wait: raise TimeoutError(Run 执行超时) if run.status requires_action: tool_calls run.required_action.submit_tool_outputs.tool_calls tool_outputs [] for call in tool_calls: if call.function.name query_order_status: import json args json.loads(call.function.arguments) # 实际业务逻辑这里用模拟数据 output f订单 {args[order_id]} 状态已发货预计明天送达 tool_outputs.append({ tool_call_id: call.id, output: output }) run client.beta.threads.runs.submit_tool_outputs( thread_idthread_id, run_idrun.id, tool_outputstool_outputs ) else: time.sleep(interval) interval min(interval * 1.5, 5) run client.beta.threads.runs.retrieve( thread_idthread_id, run_idrun.id ) if run.status completed: messages client.beta.threads.messages.list(thread_idthread_id) return messages.data[0].content[0].text.value else: raise RuntimeError(fRun 失败: {run.status})这段代码里我加了指数退避和超时保护这是实际生产环境必须的。另外注意submit_tool_outputs之后返回的 run 对象状态会变成queued需要继续轮询。5.4 多轮对话与线程复用的实操建议Thread 是可以复用的。你不需要每轮对话都创建新 Thread同一个 Thread 里追加新消息然后创建新 Run 就行。这样模型能看到完整的历史上下文。但 Thread 里的消息会一直累积token 消耗会越来越大。我的做法是当 Thread 里的消息数超过一定阈值比如 50 条就创建一个新 Thread把最近几轮的关键消息复制过去旧 Thread 归档。这样既保留了上下文又控制了 token 成本。提示Thread 本身不收费收费的是 Run 消耗的 token。但 Thread 里的消息会作为上下文传给模型所以消息越多每次 Run 的输入 token 越多。定期清理 Thread 是控制成本的有效手段。6. 常见问题与排查技巧实录6.1 Run 卡在 in_progress 或 requires_action 怎么办这是最常见的问题。首先检查网络连通性AOAI 的接口偶尔会有延迟。如果网络正常检查 Run 的状态是否真的在变化可以打印每次轮询的状态和时间戳。如果长时间停在requires_action说明你的代码没有正确处理工具调用需要检查required_action字段是否为空。还有一种情况是模型生成了工具调用但工具名称不在你定义的函数列表里。这通常是因为 instructions 里提到了某个函数但创建 Assistant 时没有把该函数加入 tools。解决办法是确保 instructions 和 tools 保持一致。6.2 代码解释器执行失败的常见原因代码解释器失败通常有几个原因一是代码里有语法错误模型生成的代码不一定总是正确的二是依赖库不存在虽然沙箱预装了很多库但一些冷门库可能没有三是执行超时复杂计算或大数据量处理容易超时四是文件路径错误模型可能用了错误的文件路径。排查方法是查看 Run 的last_error字段里面会有详细的错误信息。如果是代码错误可以在 instructions 里要求模型“生成代码后先检查语法再执行”。如果是超时建议在本地预处理数据减少沙箱的计算量。6.3 函数调用参数解析失败的排查思路模型返回的函数参数是 JSON 字符串有时候会包含格式错误比如多了逗号、少了引号。直接json.loads会抛异常。我的做法是用try-except包裹解析失败时返回一个错误描述给模型让它重新生成参数。另外如果参数里包含中文或特殊字符要确保 JSON 解析时用 UTF-8 编码。我遇到过因为编码问题导致参数解析失败的情况后来统一在解析前做encode(utf-8).decode(utf-8)处理。6.4 常见问题速查表问题现象可能原因排查方法解决方案Run 一直 queued模型部署容量不足检查部署的 TPM/RPM 配额提升配额或错峰调用Run 停在 requires_action未处理工具调用检查 required_action 字段实现 submit_tool_outputs 逻辑代码解释器超时数据量过大或计算复杂查看 last_error本地预处理数据函数参数解析失败JSON 格式错误打印原始 arguments加 try-except 并让模型重试文件下载失败文件 ID 无效或过期检查 file_id 和有效期重新上传或延长有效期多轮对话 token 超限Thread 消息过多统计消息数量和 token定期清理 Thread6.5 我踩过的三个坑和对应的解法第一个坑是文件上传后立刻使用导致报错。后来我在上传后加了 2 秒延迟并且加了重试逻辑问题就解决了。第二个坑是函数调用返回空字符串导致模型不继续。后来我统一要求函数返回值不能为空至少返回“执行成功”。第三个坑是 Run 超时没有清理导致 Thread 里堆积了很多失败的 Run。后来我在每次创建新 Run 之前先检查是否有未完成的 Run如果有就先 cancel 掉。7. 成本控制与性能优化的实战经验7.1 Token 消耗的主要来源与优化方向Assistants API 的 token 消耗主要来自三块系统指令和工具定义、Thread 历史消息、模型生成的回复和工具调用。其中 Thread 历史消息是最大的变量。一个 50 条消息的 Thread每次 Run 的输入 token 可能达到几万。优化方向有三个一是精简 instructions去掉不必要的描述二是定期清理 Thread把旧消息归档三是用文件检索代替长文本粘贴把知识库放在文件里而不是消息里。我实测过一个场景把 5000 字的业务规则从 instructions 移到文件检索每次 Run 的输入 token 从 8000 降到 2000 左右成本下降了 75%。7.2 模型选择与响应速度的平衡AOAI 提供了多种模型不同模型在速度、成本、能力上各有取舍。gpt-4o 综合能力最强但成本较高gpt-4-turbo 次之gpt-35-turbo 最便宜但复杂推理能力弱。我的建议是如果任务主要是信息提取和简单问答用 gpt-35-turbo 就够了如果需要多步推理或代码生成用 gpt-4o。另外同一个 Assistant 可以切换模型你可以先用便宜模型跑发现效果不好再换贵模型。7.3 并发调用与限流处理AOAI 有 TPM 和 RPM 限制并发太高会触发 429 错误。处理 429 的标准做法是捕获异常后等待Retry-After头指定的时间再重试。我一般会封装一个带重试的调用函数最大重试 3 次每次等待时间翻倍。另外如果业务允许可以把请求分散到多个部署上每个部署独立限流整体吞吐量能提升不少。8. 从单点能力到业务闭环的扩展思路8.1 把 Assistants API 接入现有业务系统的模式Assistants API 本身是一个独立的服务要接入业务系统通常有两种模式一种是同步模式用户请求进来后实时调用 AOAI拿到结果返回给用户另一种是异步模式用户请求先入队列后台 worker 消费队列调用 AOAI结果通过回调或轮询返回。同步模式适合交互式场景比如客服机器人异步模式适合批处理场景比如批量文档分析。我自己的项目里两种模式都有用到关键是看业务对延迟的容忍度。8.2 多 Assistant 协作的架构设计复杂业务往往需要多个 Assistant 协作比如一个负责意图识别一个负责数据查询一个负责回复生成。这时候可以用一个“调度 Assistant”来协调它根据用户输入决定调用哪个子 Assistant。但要注意Assistant 之间不能直接互相调用需要通过你的业务代码做中转。我的做法是把每个 Assistant 封装成一个服务调度层根据意图路由到对应服务服务之间通过消息队列解耦。8.3 监控与日志体系的搭建生产环境必须要有监控。我一般会记录几个关键指标每次 Run 的耗时、token 消耗、工具调用次数、失败率。这些指标可以帮助你发现性能瓶颈和成本异常。日志方面建议记录完整的 Run 生命周期包括创建时间、状态变化、工具调用参数和结果。但要注意脱敏不要把用户敏感数据写进日志。提示AOAI 的 Run 对象本身不保留历史状态你需要在每次状态变化时主动记录。建议用结构化日志方便后续做聚合分析。8.4 安全与合规的注意事项最后说几个安全方面的点。第一函数调用的参数要做校验不要直接拼接进 SQL 或命令里防止注入。第二代码解释器虽然隔离但不要上传包含敏感信息的文件。第三instructions 里不要写敏感的业务规则因为模型可能会在回复里泄露。第四定期轮换 API 密钥不要硬编码在代码里。我在实际项目里所有函数调用都会先做参数白名单校验只有符合预期格式的参数才会被执行。这个习惯帮我避免了好几次潜在的安全问题。
返回列表