只要会写python就可以学。 ——我自己说的

前言

提示工程、上下文工程、skills、MCP、tool_use、RAG、思维链、Agent。

AI应用的花园,开出如此多的花朵。 火爆的OpenClaw,其本质也是由一个一个API调用请求驱动的Agent。 一切的一切,都从一个API调用请求开始。

我将从API的调用讲起,逐步搭建一个Agent。

参考标准:OpenAI的api文档

本教程所述内容,为支持最广泛的Completion API,而非response API。前者是无状态协议,服务器不托管状态,由客户端维护对话历史,后者由OpenAI的服务器维护状态。

几乎所有的大模型(DeepSeek、Kimi、ZAI等)都支持Completion API,response API少数平台跟进,故本教程所述内容,主要针对Completion API。

DeepSeek 的API量大管饱,容易获取,因此本次教程采用DeepSeek的官方API。

基础篇:和AI搭上话

1. HTTP请求-扒开AI框架的外衣

API是各厂商包装的大模型接口,本质上是一个HTTP请求。无论是curl、Python还是nodejs,都是在模拟浏览器发起请求,并且获得回复。

curl https://api.deepseek.com/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${DEEPSEEK_API_KEY}" \
  -d '{
        "model": "deepseek-chat",
        "messages": [
          {"role": "system", "content": "You are a helpful assistant."},
          {"role": "user", "content": "Hello!"}
        ],
        "stream": false
      }'

上述指令是DeepSeek官方示例,用于发起一个简单的对话请求。其中:

  • -H表示http请求,Content-Type是第一个参数,表示请求是json格式。第二行是认证头,认证密钥。
  • -d是请求内容。是一个json格式的参数体,其中包含了model、messages等参数。 要完成一个基础的调用,本质上只需要关键的baseURL以及填入API密钥。

例如,下面是一个Python的调用示例:

from openai import OpenAI

client = OpenAI(
    api_key="sk-xxxxxx",
    base_url="https://api.deepseek.com",
)


response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "system", "content": "You are a helpful assistant"},
        {"role": "user", "content": "Hello"},
    ]
)

print(response.choices[0].message.content)

输出:图片

第九行代码print(response.choices[0].message.content)返回一个返回一个json格式体,格式如下:

response
 ├── id
 ├── object
 ├── created
 ├── model
 ├── choices    关键
 └── usage

其中choices是一个列表:

choices: [
    {
        index: 0,
        message: {
            role: "assistant",
            content: "Hello! How can I help you?"
        },
        finish_reason: "stop"
    }
]

其中的message即模型的返回内容。choices[0].message.content来获取返回的内容。 此外,chat.completion返回结构(上文的response)的其余五个字段的功能简要介绍如下:

  • id:本次请求的唯一标识符,用于日志追踪、问题排查和计费核对。
  • object:返回对象的类型标记,用于区分响应数据结构类别。
  • created:响应生成的 Unix 时间戳,用于记录请求发生时间。
  • model:实际执行推理的模型名称,用于确认调用的模型版本。
  • usage:本次请求的 token 统计信息,用于成本计算与性能分析。 在构建应用时,可以通过这些字段来记录统计以及验证、问题排查。

2. messages-交互的主舞台

messages是用户与大模型交互的主要参数,所有的历史消息,都会在messages参数中填充。一个标准的messages体如下所示:

messages: [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Hello!"},
    {"role": "assistant", "content": "Hello! How can I help you?"}
]

message是一个列表,每个元素都是一个字典,字典包含了role和content两个键值对。

  • role:角色,表示消息的发送者,常用的是system、user或assistant。
  • content:消息的具体内容,是一个字符串。

"role"参数中,最常用的有三类:system、assistant 和 user。从字面不难理解,user 代表用户端发送的消息。system 与 assistant 的区别在于:

  • system 是系统“默认”的角色设定,通常从对话开始便一直存在,可用于定义模型的角色、语言风格等基础设定。
  • assistant 则对应模型自身的回复内容,例如在网页对话中由 GPT 生成的回答就是 assistant 角色的内容。

需注意一点:在网页应用中,系统提示词(system)通常会始终保留在上下文中,不会因长度限制而被截断;而 assistant 和 user 的内容则可能因上下文过长被部分移除。

然而,通过 API 调用大模型时,模型本身不具备记忆能力,因此需要在每一轮请求的 messages 数组中重新添加 system 消息,否则模型将无法维持之前的设定。

对于一般的对话场景,使用以上三种角色已足够。

除此之外,较常用的还有 tools 角色,它们是构建智能体(Agent)功能的关键组成部分,我们将在后续内容中进一步探讨。

3. stream流式输出-让AI“边想边说”

在前面的请求中,已经调用过API,与模型进行交互。但是在运行代码的时候,由于模型生成内容需要时间,我们只能等待其输出。

从发起到得到回答,一共消耗1.13秒。 输出结果2

如果模型生成内容很长,这个时间会很久,超过10秒。例如更改prompt为“生成一个600字的小说。”

如果要我们等内容完全生成再返回,这个等待时间会非常影响体验(花了44秒多)。

生成小说

为了解决这个问题,我们可以采用流式输出(streaming)的方式。流式输出是指模型在生成内容的过程中,将内容逐步返回给客户端,而不是等待所有内容生成完毕后一次性返回。

其请求略有不同:

from openai import OpenAI

client = OpenAI(
    api_key="sk-xxxxxx",
    base_url="https://api.deepseek.com",
)


stream = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "system", "content": "You are a helpful assistant"},
        {"role": "user", "content": "Hello"},
    ],
    stream = True
)

for chunk in stream:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)

stream更改为True开启流式输出,并且获取这个流式输出的内容的方式,也与之前不同。

在流式输出中,模型会逐步返回内容,每次返回一个“chunk”,我们可以通过迭代这个流来获取每个chunk的内容。 每个chunk的的结构体如下所示:

chunk = {
  "id": "chatcmpl-xxx",
  "object": "chat.completion.chunk",
  "created": 1700000000,
  "model": "deepseek-chat",
  "choices": [
    {
      "index": 0,
      "delta": {...},
      "finish_reason": null
    }
  ]
}

可以看见,其中大部分的对象功能,和非流式对象ChatCompletion一致(id、object、model等),不同的是其中的choices属性,其中choices[0]不再包含messages,而是delta。delta是一个字典,包含了当前chunk的内容,可能是一个或若干个token,也可能是role等值,例如,delta可能是下面的结构体(“输出一半的Hello”):

"delta": {
  "content": "Hel"
}

这是一种通过SSE协议增量更新、单向通信的形式,提供更低延迟的传输。

传输过程可以用下面的时序呈现:

Client                         Server
  |                               |
  |  POST /chat.completions       |
  |  stream=True                  |
  |------------------------------->|
  |                               |
  |<---- data: {role:assistant} --|
  |<---- data: {"Hel"} ----------|
  |<---- data: {"lo"} -----------|
  |<---- data: {finish:stop} ----|
  |<---- data: [DONE] -----------|
  |                               |
Connection stays open until DONE

其中,第一个chunk的delta中,role为assistant,内容为空,代表模型开始生成回复。中途,模型会逐步返回内容,每个chunk的delta中,content包含了当前生成的token。最后,当模型生成完成时,会返回一个finish_reason为stop的chunk,代表模型生成结束。

值得注意的是,SSE是一种单向传输协议,并且传输的内容时间上不是完整的json格式体,而是文本流,用两个换行符表示每个chunk的结束。而本地的Python代码中,我们需要手动解析这个文本流,将每个chunk的内容提取出来。OpenAI的库封装了提取逻辑,stream = client.chat.completions.create(...)这行代码提取出了每个chunk。

以下是流式输出的示例,展示了模型生成内容的过程:

data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"},"index":0}]}

data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hel"},"index":0}]}

data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"delta":{"content":"lo"},"index":0}]}

data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop","index":0}]}

data: [DONE]

其中,[DONE]是一个特殊的chunk,代表模型生成结束。

4. 思考模式-偷听大模型在想什么

目前很多模型都带有思考模式,例如deepseek-chat、MiniMax-M2.5等。

但由于安全性,还有商业化等原因,OpenAI 的 ChatCompletion API 中并没有提供直接的 thinking 模式的接口,而 Deepseek 和小米等一些开源的厂商使用额外“extra_body”来调整thinking参数,获取模型的思考过程,例如:

stream = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "system", "content": "You are a helpful assistant"},
        {"role": "user", "content": "生成一个七言律诗,赞美中国"},
    ],
    stream=True,
    extra_body={
        "thinking": {"type": "enabled"}
    }
)

而有一些厂商,例如智谱(ZAI),他们在 API 的后端也提供了 thinking 的转化,可以直接使用thinking来调整思考模式的启用与否。

总之,对于支持思考的模型,通过thinking标签或者extra_body标签的thinking标签,可以进行思考模式的启用。

思考模式的返回内容,由reasoning_content承接,在流式chunk中,替代content字段。

Client                                Server
  |                                       |
  |  POST /chat/completions               |
  |  stream=True                          |
  |-------------------------------------->|
  |                                       |
  |<---- data: {role: assistant} ---------|
  |                                       |
  |<---- data: {reasoning: "Let me "} ----|
  |<---- data: {reasoning: "check the "} -|
  |<---- data: {reasoning: "weather..."} -|
  |                                       |
  |<---- data: {content: "The "} ---------|
  |<---- data: {content: "weather "} -----|
  |<---- data: {content: "is sunny."} ----|
  |                                       |
  |<---- data: {finish_reason: stop} -----|
  |<---- data: [DONE] --------------------|
  |                                       |
Connection stays open until DONE

其中,前半段的reasoning_content是模型的思考内容,后半段的content是模型的回复内容。因此在流式输出中,我们可以通过判断chunk.choices[0].delta.reasoning_content是否存在,来获取模型的思考内容。

5. 结构化输出-让大模型“站军姿”

LLM的输出,是一个个的token,本质上是文本流,是一种非结构化的数据。

而非结构化的数据难以利用,我们可以通过一些措施来生成结构化的输出。

最简单的就是使用系统提示词约束,要求其依照特定的模板进行填空,得到结构化的json文本。

例如这样一个系统提示词:

用json格式来提取发送文本中的人物、地点、时间,直接输出json,不要输出其余内容。
参考:
{
  'name': 小张,
  'place': 教室,
  'time': 中午12点
}

此时发送一段文本就可以获得json结构的输出(图中是直接在chatgpt网页进行对话。):

结构化输出

如果批量调用API,那么大模型就可以成为一个文本处理组件,进行自动化的批量文本处理。

然而,由于众所周知的“幻觉”问题,大模型有时并不会按照约束进行输出,此时就需要用“失败重试”等机制来进行错误校验。为了加强大模型的约束,减少开发者的开发负担,一些厂商推出了结构化输出(大多是JSON格式输出)。

例如DeepSeek的文档中,在发送的请求中,需要设置response_format 参数的值为{'type': 'json_object'} 来开启结构化输出。但是,在文档中,依然提到“在使用 JSON Output 功能时,API 有概率会返回空的 content。我们正在积极优化该问题,您可以尝试修改 prompt 以缓解此类问题。”

因此,还是需要开发者来进行测试(🤣),同时,也并不是所有的模型都支持response_format 参数。 在使用response_format 参数时,依然需要提供对应的json模板。

import json
from openai import OpenAI

client = OpenAI(
    api_key="sk-a9c7cee0e60947708xxxxx",
    base_url="https://api.deepseek.com",
)

system_prompt = """
用户会提供一个纯文本,你需要从文本中提取出内容,并以 JSON 格式输出。

示例输入: 
小张12点在教室睡觉。

EXAMPLE JSON OUTPUT:
{
    "name": "小张",
    "place": "教室",
    "time": "12点",
    "event": "睡觉"
}
"""

user_prompt = "小红元旦那天在教室跳热舞。"

messages = [{"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}]

response = client.chat.completions.create(
    model="deepseek-chat",
    messages=messages,
    response_format={
        'type': 'json_object'
    },
    stream=True
)

for chunk in response:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)

输出结果如下所示:

结构化输出2