← Back to 文字

LLM 基本概念与 DeepSeek 接入

本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。

学习目标

学完本节后,你应当能够:

  • 理解一次 LLM 请求的最小组成。
  • 了解 DeepSeek 的 OpenAI 兼容接口使用方式。
  • httpx 发出一次最小聊天请求。
  • 理解为什么课程里要做“DeepSeek 主线 + 通用抽象”。

前置知识

  • 已完成 Python 基础与工程化模块

1. 一次 LLM 请求最少包含什么

一次聊天类 LLM 请求,至少包含以下内容:

  • model
  • messages
  • 可选的生成参数,例如 temperature

最核心的是 messages。它不是单纯的一句话,而是一组对话消息。

2. OpenAI 兼容接口是什么意思

DeepSeek 官方提供与 OpenAI 风格兼容的聊天接口。你可以把它理解为:

  • 请求结构和多数 OpenAI Chat Completions 风格类似
  • 可以复用相同的数据结构设计
  • 可以为后续切换模型供应商留出空间

但“兼容”不等于“一切完全相同”。因此课程中仍然要求把厂商差异收口到 client 层。

3. 当前课程采用的默认配置

本月课程默认采用以下配置:

DEEPSEEK_API_KEY=你的密钥
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-chat

说明:

  • 课程写作时,DeepSeek 官方文档仍然将 https://api.deepseek.com 作为主要兼容接口地址。
  • deepseek-chat 适合作为月份 1 的默认模型,因为它更符合普通聊天与结构化输出场景。

4. 最小请求结构

{
  "model": "deepseek-chat",
  "messages": [
    {"role": "system", "content": "你是一个严谨的研发助手。"},
    {"role": "user", "content": "请介绍一下 FastAPI。"}
  ],
  "temperature": 0.2
}

5. 用 httpx 发送最小请求

示例代码:

import os

import httpx
from dotenv import load_dotenv


load_dotenv()


async def call_deepseek() -> str:
    api_key = os.getenv("DEEPSEEK_API_KEY")
    base_url = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
    model = os.getenv("DEEPSEEK_MODEL", "deepseek-chat")

    if not api_key:
        raise ValueError("DEEPSEEK_API_KEY 未配置")

    payload = {
        "model": model,
        "messages": [
            {"role": "system", "content": "你是一个严谨的研发助手。"},
            {"role": "user", "content": "请介绍一下 FastAPI。"},
        ],
        "temperature": 0.2,
    }

    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }

    async with httpx.AsyncClient(timeout=30.0) as client:
        response = await client.post(
            f"{base_url}/chat/completions",
            headers=headers,
            json=payload,
        )
        response.raise_for_status()
        data = response.json()
        return data["choices"][0]["message"]["content"]

执行入口:

import asyncio


if __name__ == "__main__":
    print(asyncio.run(call_deepseek()))

6. 代码讲解

为什么不用 SDK 开始

因为本周目标是先理解请求本质:

  • URL
  • headers
  • body
  • 响应 JSON

先掌握这一层,再决定是否接 SDK 或框架。

为什么 response.raise_for_status() 很重要

因为它会在 4xx/5xx 时尽早抛异常,避免你误把错误响应当成正常结果继续解析。

为什么把 base_urlapi_keymodel 都做成配置

因为课程要求你形成“模型提供方可替换”的工程习惯。

7. LLM 请求生命周期图

flowchart LR
    A["读取 .env"] --> B["构造 messages"]
    B --> C["组织请求体 JSON"]
    C --> D["发送 HTTP 请求"]
    D --> E["收到响应 JSON"]
    E --> F["提取 answer"]
    F --> G["记录日志/成本/错误"]

8. 实操任务

  1. llm_api_labprompt_lab 中新建 deepseek_demo.py
  2. 复制并运行最小请求
  3. 把 system prompt 改成“你是 Python 导师”,观察输出变化
  4. 故意把 API Key 改错,观察报错

9. 自测题

  1. 为什么课程先用 httpx 而不是一开始就用 SDK?
  2. OpenAI 兼容接口给工程结构带来的核心好处是什么?
  3. 为什么 messages 比单一 prompt 字符串更适合真实对话?

10. 作业与验收

作业:

  • 封装一个最小 LLMClient.generate() 方法,只完成普通文本返回即可。

验收标准:

  • 能返回模型文本
  • 配置来自 .env
  • 网络错误和鉴权错误能清晰暴露

11. 常见错误

  • base_url 写成错误地址
  • 忘记附带 Authorization
  • 错把响应字段名理解为固定不变且无需检查

12. 本章与前文关系

前面的 Python 基础与工程化章节,目的是让你具备:

  • 稳定本地环境
  • 项目结构
  • 配置读取
  • 数据建模
  • 测试意识

现在才真正进入月份 1 的“AI 应用开发”核心:模型调用。

但请注意,本章的目标不是“会调一次接口”,而是建立完整请求链路直觉:

  • 请求发到哪里
  • 头里带什么
  • body 长什么样
  • 响应怎么拆
  • 出错时在哪里报

13. 本章在研发助手项目中的位置

研发助手项目中的 llm_client.py,本质上就是本章内容的工程化版本。无论后面你做:

  • 总结 diff
  • 生成 commit message
  • 抽取待办项

都要先经过本章建立的调用链路。

14. 基于官方文档确认的 DeepSeek 兼容接口事实

本课程写作时,DeepSeek 官方文档明确说明:

  • 兼容 OpenAI 风格 API
  • 主要 base_urlhttps://api.deepseek.com
  • 为兼容某些现有工具,也可使用 https://api.deepseek.com/v1
  • 常见模型包括 deepseek-chatdeepseek-reasoner

这里有两个需要讲清的边界:

第一,兼容的是“接口风格”,不是“一切语义完全等同”

也就是说,你可以复用大体请求结构,但不能不看官方文档就默认所有参数、行为和高级功能完全一致。

第二,/v1 只是兼容路径,不代表模型版本

这点在官方文档里有明确说明。很多初学者会误以为 /v1 和模型版本强绑定,这是错误理解。

来源:

15. 一次模型调用的 anatomy

如果你只记“POST 一个 JSON”,很快就会在调试时迷路。更好的理解方式是拆成 5 层:

第一层:配置层

  • API Key
  • Base URL
  • 默认模型名

第二层:输入层

  • messages
  • 生成参数,如 temperature

第三层:传输层

  • HTTP 方法
  • URL
  • headers
  • JSON body

第四层:响应层

  • 状态码
  • 响应 JSON
  • 模型生成文本

第五层:应用层

  • 日志
  • 成本记录
  • 错误处理
  • 向上返回给 service 或 CLI

这 5 层分开理解,后面你才不会把所有问题都归因于“模型不稳定”。

16. 错误示例 vs 正确示例

错误示例:直接把调用逻辑写死在脚本里,不抽象边界

import httpx
import os


async def main():
    response = await httpx.AsyncClient().post(
        "https://api.deepseek.com/chat/completions",
        headers={"Authorization": f"Bearer {os.getenv('DEEPSEEK_API_KEY')}"},
        json={"model": "deepseek-chat", "messages": [{"role": "user", "content": "hello"}]},
    )
    print(response.json())

这段代码不是不能跑,但问题很明显:

  • 配置没有收口
  • 没有统一错误处理
  • 没有请求边界抽象
  • 后面很难复用到 FastAPI 或项目 service 层

正确示例:抽成最小 LLMClient

class LLMClient:
    async def generate(self, messages: list[dict[str, str]]) -> str:
        ...

课程后续都将围绕这种边界去组织代码。

17. 完整文件级示例:app/clients/llm_client.py

"""月份 1 的最小 DeepSeek 客户端实现。"""

from __future__ import annotations

import os
from collections.abc import AsyncIterator
from typing import Any

import httpx
from dotenv import load_dotenv


load_dotenv()


class LLMClient:
    """封装月份 1 需要的最小模型调用能力。

    当前阶段先聚焦普通文本生成和流式生成。
    结构化输出和 Tool Calling 会在后续章节继续扩展。
    """

    def __init__(self) -> None:
        self.api_key = os.getenv("DEEPSEEK_API_KEY")
        self.base_url = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
        self.model = os.getenv("DEEPSEEK_MODEL", "deepseek-chat")

        if not self.api_key:
            raise ValueError("DEEPSEEK_API_KEY 未配置")

    @property
    def headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }

    async def generate(
        self,
        messages: list[dict[str, str]],
        temperature: float = 0.2,
    ) -> str:
        """执行一次普通文本生成。"""

        payload = {
            "model": self.model,
            "messages": messages,
            "temperature": temperature,
        }

        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(
                f"{self.base_url}/chat/completions",
                headers=self.headers,
                json=payload,
            )
            response.raise_for_status()

        data = response.json()
        return data["choices"][0]["message"]["content"]

    async def stream(
        self,
        messages: list[dict[str, str]],
        temperature: float = 0.2,
    ) -> AsyncIterator[str]:
        """按 chunk 逐块返回文本。

        这里为了教学简洁,只演示最小消费逻辑。
        真正生产级实现还需要考虑更完整的事件格式和关闭逻辑。
        """

        payload = {
            "model": self.model,
            "messages": messages,
            "temperature": temperature,
            "stream": True,
        }

        async with httpx.AsyncClient(timeout=30.0) as client:
            async with client.stream(
                "POST",
                f"{self.base_url}/chat/completions",
                headers=self.headers,
                json=payload,
            ) as response:
                response.raise_for_status()

                async for line in response.aiter_lines():
                    if not line or not line.startswith("data: "):
                        continue

                    if line == "data: [DONE]":
                        break

                    yield line

18. 逐段解释这份完整示例

为什么 LLMClient 在初始化时就读配置

因为后面所有调用都依赖同一组配置。把它们放进对象初始化,可以减少上层逻辑重复传参。

为什么 headers 单独抽成属性

因为:

  • 逻辑重复
  • 易于统一修改
  • 更利于阅读

为什么先实现 generate()

因为它最简单,也最容易建立调用链路直觉。后面的 structured_generate()call_with_tools() 都是在它的基础上扩展。

为什么 stream() 返回 AsyncIterator[str]

这正是为了让上层可以:

  • 在 CLI 中逐块打印
  • 在 FastAPI 中逐块透传

这一步已经在为后面的服务化章节铺垫。

19. 从最小实现到工程实现的递进

你现在应该把模型调用理解成一个逐步升级的过程:

第一步:普通文本生成

理解请求和响应最小结构。

第二步:结构化输出

让模型为程序返回稳定结构。

第三步:流式输出

让结果边生成边消费。

第四步:工具调用

让模型不只输出文本,还能触发程序能力。

这也是后面几个章节的组织逻辑。

20. 调试与排错:本章最常见问题

问题一:401 Unauthorized

优先检查:

  • .env 是否加载
  • API Key 是否为空
  • Authorization 是否是 Bearer <key> 形式

问题二:404 或路径错误

优先检查:

  • base_url 是否为 https://api.deepseek.com
  • 是否拼成了错误路径
  • 是否误把 /v1 当作必须路径

问题三:响应解析报错

优先检查:

  • 状态码是否成功
  • 响应 JSON 是否真的包含 choices
  • 是否误把错误响应当成正常响应

21. 本章完成后你应该具备的能力

完成本章后,你应当做到:

  1. 能独立描述一次 LLM 请求的完整链路。
  2. 能写出最小 LLMClient.generate()
  3. 能解释 DeepSeek 的 OpenAI 兼容接口意味着什么、不意味着什么。
  4. 能初步读懂流式返回和普通返回的差异。

22. 如果你卡在这里,先回看哪几章

23. 从本章过渡到下一章的桥接说明

接下来进入 02-消息结构与上下文管理.md

因为你现在已经会“发请求”,下一步必须搞清楚“发的到底是什么”。也就是:为什么 messages 不是一段普通字符串,为什么多轮对话需要手动维护历史,以及为什么消息结构是后面 Tool Calling 和 FastAPI schema 的共同基础。

Fin