拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
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_endpoint="https://your-resource.openai.azure.com/", api_key="your-api-key", api_version="2024-05-01-preview" ) # 上传文件 file = client.files.create( file=open("sales_data.csv", "rb"), purpose="assistants" ) print(file.id) # 创建带代码解释器的 Assistant assistant = client.beta.assistants.create( name="数据分析助手", instructions="你是一个数据分析助手,使用代码解释器分析用户上传的数据。", model="gpt-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_workers=5) 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 openai>=1.30.0

环境变量建议这样配置,避免把密钥硬编码在代码里:

export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" export AZURE_OPENAI_API_KEY="your-api-key" export AZURE_OPENAI_API_VERSION="2024-05-01-preview" export AZURE_OPENAI_DEPLOYMENT="gpt-4o"

我习惯把 deployment 名称也做成环境变量,因为不同环境(开发、测试、生产)可能用不同的部署名,硬编码会导致切换环境时改代码。

5.2 创建 Assistant 与 Thread 的完整代码

下面是一个完整的初始化流程,包含 Assistant 创建、Thread 创建、消息追加。

import os from openai import AzureOpenAI client = AzureOpenAI( azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), api_key=os.getenv("AZURE_OPENAI_API_KEY"), api_version=os.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 函数。", model=os.getenv("AZURE_OPENAI_DEPLOYMENT"), tools=functions ) # 创建 Thread thread = client.beta.threads.create() # 追加用户消息 client.beta.threads.messages.create( thread_id=thread.id, role="user", 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_id=thread_id, assistant_id=assistant_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_id=thread_id, run_id=run.id, tool_outputs=tool_outputs ) else: time.sleep(interval) interval = min(interval * 1.5, 5) run = client.beta.threads.runs.retrieve( thread_id=thread_id, run_id=run.id ) if run.status == "completed": messages = client.beta.threads.messages.list(thread_id=thread_id) return messages.data[0].content[0].text.value else: raise RuntimeError(f"Run 失败: {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定期清理 Thread

6.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 密钥,不要硬编码在代码里。

我在实际项目里,所有函数调用都会先做参数白名单校验,只有符合预期格式的参数才会被执行。这个习惯帮我避免了好几次潜在的安全问题。

返回列表