Jean's Blog

一个专注软件测试开发技术的个人博客

0%

LLM API模型调用服务

阅读路线:先调用模型,再构建智能体

调用模型是 Agent 的基础,但一次模型调用不等于一个 Agent。本系列把原来的长文拆成四篇,每篇解决一个问题:

顺序 文章 学习目标
1 本篇:LLM API 模型调用服务 读懂一次请求与响应
2 生成参数、Token 与上下文管理 控制随机性和预算,维护上下文
3 结构化输出与工具调用 把模型建议转换成经过校验的动作
4 错误处理与模型适配器 处理失败,隔离供应商差异

前置知识:能阅读 Python、JSON,知道 HTTP 请求与响应的基本概念即可。

核对日期:2026-09-21。 通用概念与厂商实现分开说明。示例使用 OpenAI Python SDK,但连接 DeepSeek 时,服务提供方仍是 DeepSeek。API、模型别名及参数支持会变化,请以对应厂商文档为准。

示例验证范围:代码使用 OpenAI Python SDK 3.0.0、Pydantic 2.13.4 做了语法和模拟响应测试,未调用付费模型。实际运行仍需有效密钥、账户权限与兼容模型。

1. LLM API 在 Agent 中负责什么

可以把基础 Agent 理解为一个由应用程序控制的循环:整理上下文 → 请求模型 → 检查结果 → 必要时执行工具 → 把观察结果交回模型。

graph TD
    A[用户提出任务] --> B[应用整理指令与上下文]
    B --> C[调用 LLM API]
    C --> D{检查模型响应}
    D -->|最终回答| E[返回用户]
    D -->|工具调用请求| F[应用校验参数和权限]
    F -->|允许| G[执行工具]
    G --> H[记录工具结果]
    H --> B
    D -->|拒答或异常| I[停止或按策略处理]
    F -->|拒绝| I

图中的 LLM 负责生成回答或提出工具调用请求;应用负责权限、执行、状态和停止条件。工具可以是查询数据库、读取文件等函数。自定义函数不会因为出现在模型响应中,就自动在你的电脑上运行。[1]

本文先把“调用模型”这一格讲清楚,工具闭环放到第三篇。

2. 先分清模型、API 和 SDK

名词 是什么 不代表什么
模型 实际执行推理的模型或服务别名 不等于接口协议
API 请求与响应的协议,例如 Chat Completions、Responses 不等于某个 Python 包
SDK 帮助构造请求、解析响应的客户端库 安装了 SDK,不表示服务端支持 SDK 的全部方法
兼容接口 另一家服务实现了某种请求/响应格式 不表示参数、状态管理、工具能力完全一致

例如,OpenAI(base_url=...) 可以连接实现相应协议的其他服务。能调用 client.chat.completions.create(),不能单凭这一点推断它也支持 client.responses.create()。

本次核对时,DeepSeek 官方已经支持 Responses API,所以原文用 OpenAI SDK 调 DeepSeek Responses 的方向没有错;需要补充的是兼容范围,而不是把它误判为“不支持”。[2]

3. Chat Completions:以消息列表为中心

3.1 messages 是本次请求的上下文

1
2
3
4
5
6
7
8
9
10
11
请求:messages[]
├─ system / developer:应用指令,具体支持因模型而异
├─ user:用户请求或提供的数据
├─ assistant:模型先前的回答、工具调用请求
└─ tool:自定义工具的执行结果

响应:choices[]
└─ message
├─ content:文本,可能为空
├─ tool_calls:工具调用请求,可能有多个
└─ refusal:拒答标记,是否提供取决于服务

choices[] 表示候选结果。普通单候选请求通常读取第一项,但message.content 不是整个响应的唯一入口:工具调用可能放在 tool_calls 中,文本也可能为 None。还要读取 finish_reason 和 usage。[3][4]

role 工程含义 注意事项
system 应用设置的全局指令 不要把外部网页、工具结果提升成系统指令
developer OpenAI 部分模型使用的开发者指令角色 不是所有兼容服务都支持相同语义
user 用户请求及相关输入 不能仅凭 role 判断内容可信
assistant 模型先前的输出 工具调用也属于模型输出
tool 工具返回的观察结果 用调用 ID 与相应工具请求关联

把消息全部拼成一个字符串并非技术上“无法调用”,但会丢失协议层的来源、角色及工具关联信息,不利于上下文管理。角色分层是组织信息的手段,不是阻止提示注入的安全沙箱。

3.2 最小可运行示例:DeepSeek Chat Completions

本系列使用 Python 3.10+。在自己的示例项目中安装依赖,不需要安装 Agent 框架:

1
python -m pip install openai python-dotenv 'pydantic>=2,<3'

在示例项目目录创建 .env,并把它加入 .gitignore:

1
2
3
DEEPSEEK_API_KEY=替换为你自己的密钥
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL_NAME=deepseek-flash

模型名说明:截至 2026-09-21 核对日,官方推荐使用 deepseek-flash。原文中的 deepseek-v4-flash 仍被接受,但已经作为旧别名路由到 DeepSeek-V4.1-Flash;不能再用这个别名声称复现了旧模型实验。[5]

下面代码可独立保存为 Python 文件执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import os

from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
timeout=30.0,
max_retries=0, # 教学示例先显式暴露失败,第四篇再讨论重试
)

response = client.chat.completions.create(
model=os.environ["DEEPSEEK_MODEL_NAME"],
messages=[
{"role": "system", "content": "你是一名诗人,只输出四句诗。"},
{"role": "user", "content": "请写一首以秋天为主题的七言绝句。"},
],
max_tokens=600,
extra_body={"thinking": {"type": "disabled"}}, # DeepSeek 专属参数
)

if not response.choices:
raise RuntimeError("没有返回候选结果")

choice = response.choices[0]
message = choice.message
if choice.finish_reason != "stop":
raise RuntimeError(f"本示例只接受完整文本响应:{choice.finish_reason}")
if message.refusal:
raise RuntimeError(f"模型拒答:{message.refusal}")
if message.tool_calls:
raise RuntimeError("本示例没有提供工具,不应执行工具请求")
if not message.content:
raise RuntimeError("未返回文本,不能当作成功回答")

print(message.content)
print("finish_reason:", choice.finish_reason)
print("usage:", response.usage)

这里使用的是 OpenAI SDK + DeepSeek API,不是“DeepSeek 官方 SDK”。max_tokens 和 thinking 按 DeepSeek 文档配置;换供应商时不能无条件照搬。[4]

真实诗句、token 数和延迟每次可能不同。教学上应观察“文本、结束原因、用量”三个部分,不必把一整段 SDK 对象打印日志当成接口规范。

4. Responses:以类型化输入项和输出项为中心

4.1 input 不只是 messages 改名

Responses 的 input 可以是字符串,也可以是输入项列表。列表既能承载带 role 的消息,也能承载函数调用结果等项目;output[] 则是一次响应中的输出项序列,不是多个候选答案。[6]

1
2
3
4
5
6
7
8
9
10
11
12
response
├─ status:completed / incomplete / failed 等状态
├─ output[]
│ ├─ type: reasoning 推理相关项,内容因模型而异
│ ├─ type: function_call 请求应用执行函数
│ └─ type: message
│ └─ content[]
│ ├─ type: output_text
│ └─ type: refusal
├─ usage
├─ error
└─ incomplete_details

这是形状示意,不表示每次都会返回全部项目,也不表示输出项顺序固定。

需要纠正三个常见误解:

  1. 仍然可以使用 system / developer 消息,不是只能有 user / assistant。顶层 instructions 是设置指令的便利方式,不意味着 input 禁止系统消息。[2][6]
  2. 拒答通常是 message.content[] 中的 refusal 内容块,不是与 message 并列的顶层输出项。[7]
  3. 不要固定访问 output[0].content[0].text。首项可能是推理或工具请求;纯文本展示可用 Python SDK 的 response.output_text,调度逻辑则要检查状态和项目类型。[6]

output_text 是 SDK 汇总文本的便利属性,不要假设所有原始 HTTP 响应都包含同名顶层字段。它也不会替你执行工具或处理失败。

4.2 最小可运行示例:DeepSeek Responses

沿用上面的 .env,下面是另一个可独立运行的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import os

from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
timeout=30.0,
max_retries=0,
)

response = client.responses.create(
model=os.environ["DEEPSEEK_MODEL_NAME"],
instructions="你是一名诗人,只输出四句诗。",
input="请写一首以秋天为主题的七言绝句。",
max_output_tokens=600,
reasoning={"effort": "none"}, # 此处按 DeepSeek 的参数支持配置
)

if response.status != "completed":
raise RuntimeError(
f"响应未完成:{response.status}; "
f"error={response.error}; details={response.incomplete_details}"
)

for item in response.output:
if item.type == "message":
for part in item.content:
if part.type == "refusal":
raise RuntimeError(f"模型拒答:{part.refusal}")
elif item.type != "reasoning":
raise RuntimeError(f"本纯文本示例不处理输出项:{item.type}")

if not response.output_text:
raise RuntimeError("未返回可展示的文本")

print(response.output_text)
print("status:", response.status)
print("usage:", response.usage)

这两个脚本都刻意限定为“非流式、无工具、纯文本”。生产应用应分类处理异常,不是把所有情况永久转换成同一种 RuntimeError。

4.3 兼容 Responses,不等于拥有全部 OpenAI 功能

根据 DeepSeek 核对日的兼容说明:[2]

能力 DeepSeek Responses 的边界
文本输入输出、自定义函数调用 支持
previous_response_id、conversation 不支持服务端会话续接,需要应用回传上下文
store、background 不支持,不能据此设计持久会话或后台任务
OpenAI 内置 web_search、file_search 等工具 不能按 OpenAI 能力使用,兼容表标为忽略
developer 消息角色 当前按 user 处理,不能假设与 OpenAI 指令优先级相同
未支持参数 某些参数被静默忽略,HTTP 成功不代表参数生效

因此,多供应商应用除了统一字段,还需要确认实际能力与语义。

5. 如何选择 Chat Completions 与 Responses

维度 Chat Completions Responses
输入组织 messages[] 字符串或输入项列表,可用 instructions
输出组织 choices[] 中的消息 output[] 中的类型化输出项
读取纯文本 检查状态后读取 message.content 检查状态后使用 SDK 的 output_text
自定义工具请求 message.tool_calls output[] 中的 function_call
自定义工具执行者 应用程序 应用程序
多轮状态 通常由应用维护消息 OpenAI 可使用会话关联;其他供应商要查兼容表
多模态 取决于模型与端点能力 同样取决于模型与端点能力

不要按“聊天用 Chat、Agent 必须用 Responses”机械划分。 两者都可以支持工具循环。对 OpenAI 新项目,官方推荐 Responses;对兼容生态、已有应用或特定供应商,应以所需能力、迁移成本及评测结果选型。[1][6]

OpenAI Responses 中可通过 previous_response_id 关联前次响应,但这不是无限记忆,也不是历史上下文从此不消耗预算。跨请求的应用指令应按接口约定明确维护。[8]

6. 流式输出:增量不等于完整结果

非流式调用返回完整响应;流式调用逐步收到事件或片段。界面可以边收边展示,但程序不能把任意片段当作完整 JSON 或可执行工具参数。

  • Chat Completions 通常读取 choices[].delta,不是每个 chunk 都有完整 message。
  • Responses 按事件类型读取,例如 response.output_text.delta。
  • 需要等到终止状态,确认是否完整、失败或截断;连接断开不代表正常结束。
  • 工具参数必须收集完整并校验后才能执行;用量是否在最后返回,要看厂商实现。[2][4]

7. 本篇小结

记住三层即可:SDK 是客户端,API 是协议,模型是能力来源。

读一次响应时,先问:

  1. 请求有没有成功到达并完成?
  2. 返回的是文本、拒答,还是工具调用?
  3. 有没有截断?消耗了多少 token?

下一篇:生成参数、Token 与上下文管理。

参考资料

以下均为官方资料,核对日期为 2026-09-21;兼容矩阵随服务更新而变化。

  1. OpenAI:Function calling
  2. DeepSeek:Using the Responses API
  3. OpenAI:Chat Completions API
  4. DeepSeek:Chat Completions API
  5. DeepSeek:Models & Pricing
  6. OpenAI:Migrate to the Responses API
  7. OpenAI:Structured model outputs
  8. OpenAI:Conversation state