ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI 应用开发实战(1):用 FastAPI + TaoToken 统一 Key 从零搭建第一个 LLM API 应用

AI 应用开发实战(1):用 FastAPI + TaoToken 统一 Key 从零搭建第一个 LLM API 应用

1. 为什么零基础要先搭一个“能跑起来”的 LLM API 应用

很多人学 AI 应用开发,第一步就卡在“调 API”这件事上:文档看了一堆,Key 申请了好几个,代码复制过来却跑不通,报错信息还看不懂。问题往往不在模型本身,而在于缺少一个最小可运行骨架——一个能把请求发出去、把结果收回来、出错时知道去哪查的完整闭环。

FastAPI 就是搭这个骨架最省事的工具。它用 Python 类型注解做参数校验,自带交互式接口文档,异步支持好,几十行代码就能起一个 HTTP 服务。配合一个统一的 LLM API 入口,你不需要为每个模型厂商写一套调用逻辑,换模型只改配置不改业务代码。

这篇文章面向的是完全没接触过 LLM API 的读者:会一点 Python 基础,装过 pip,但没写过 Web 服务也没关系。我们会从目录结构开始,一步步写出可复制的路由、配置和错误处理,最后用 curl 和浏览器把接口跑通。你最终得到的是一个能对话、能流式输出、出错有明确提示的 AI 应用骨架,后续加前端、加记忆、加工具调用都在这上面长。

我试过把调用逻辑直接写在路由里,结果换模型时改了七八处,还漏了一处导致线上报错。所以这篇的重点之一,就是把 LLM 调用收敛到一个文件里,用统一 Key 和统一 Base URL 管理,这也是后面所有实战的地基。

2. TaoToken 前置准备:统一 Key 与 Base URL 怎么拿

TaoToken 在这里扮演的角色是统一的模型接入层:你只需要一个 API Key 和一个 Base URL,就能用 OpenAI 兼容的 SDK 调用不同模型。对零基础读者来说,好处是不用分别注册多个厂商、不用记多套鉴权方式,代码里的client只初始化一次。

第一步,打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册并登录。登录后进入控制台,找到 API Keys 页面(deep link:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite),创建一个新 Key。Key 只在创建时完整显示一次,复制后立刻存到本地密码管理器或临时文本里,不要贴在聊天窗口或提交到 Git。

第二步,确认 Base URL。TaoToken 的 API 入口是 https://taotoken.net/api(这个地址不加 UTM 参数,直接作为 SDK 的base_url使用)。注意末尾不要自己补/v1,SDK 会按 OpenAI 兼容规范拼接路径,多写一段反而会 404。

第三步,确认你要用的 Model ID。在模型对话页面(deep link:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite)可以看到当前可用的模型列表,把模型名称原样记下来,比如gpt-4o-mini这类字符串,后面写进.env的LLM_MODEL。

这里有个容易踩的坑:Key、Base URL、Model ID 是三件套,缺一不可,而且必须来自同一个入口。有人把 A 平台的 Key 配 B 平台的 Base URL,请求会直接返回 401;有人 Model ID 拼错一个字母,报错是model_not_found。所以建议你在.env里把三项写在一起,注释清楚来源,换环境时整段替换。

安全提醒:.env必须写进.gitignore。生产环境不要把 Key 硬编码进镜像,用环境变量或密钥管理服务注入。TaoToken 是正规的模型接入服务,Key 泄露同样有被滥用的风险,这一点和任何 API 服务一样。

3. 可复制配置:项目结构、依赖与 FastAPI 路由

先建目录。结构尽量扁平,零基础读者一眼能看懂每个文件干什么:

ai-chat-app/ ├── main.py # FastAPI 入口与路由 ├── llm.py # LLM 调用封装(唯一改模型的地方) ├── requirements.txt ├── .env # 本地配置,不提交 Git ├── .gitignore └── static/ └── index.html # 可选的最小前端

requirements.txt固定版本,避免今天能跑明天依赖升级挂掉:

fastapi==0.115.0 uvicorn[standard]==0.30.0 python-dotenv==1.0.0 openai==1.50.0

安装命令:

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt

.env三件套加站点名,路径与项目根目录一致:

LLM_API_KEY=sk-你的TaoToken密钥 LLM_BASE_URL=https://taotoken.net/api LLM_MODEL=你的模型ID SITE_NAME=AI Chat

.gitignore至少包含:

.env venv/ __pycache__/

接下来是核心的llm.py。所有模型调用都收在这里,业务代码只调函数,不碰 SDK:

import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"), ) MODEL = os.getenv("LLM_MODEL") def chat_sync(messages, temperature=0.7, max_tokens=1024): resp = client.chat.completions.create( model=MODEL, messages=messages, temperature=temperature, max_tokens=max_tokens, ) return resp.choices[0].message.content def chat_stream(messages, temperature=0.7, max_tokens=1024): resp = client.chat.completions.create( model=MODEL, messages=messages, temperature=temperature, max_tokens=max_tokens, stream=True, ) for chunk in resp: if chunk.choices and chunk.choices[0].delta.content: yield chunk.choices[0].delta.content

main.py提供两个路由:非流式/api/chat和流式/api/chat/stream,外加一个健康检查/health方便验证服务是否起来:

import os from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, StreamingResponse from dotenv import load_dotenv from llm import chat_sync, chat_stream load_dotenv() app = FastAPI(title=os.getenv("SITE_NAME", "AI Chat")) @app.get("/health") async def health(): return {"status": "ok", "model": os.getenv("LLM_MODEL")} @app.post("/api/chat") async def chat(request: Request): body = await request.json() message = (body.get("message") or "").strip() if not message: return JSONResponse({"error": "message 不能为空"}, status_code=400) try: reply = chat_sync([{"role": "user", "content": message}]) return {"reply": reply} except Exception as e: return JSONResponse({"error": str(e)}, status_code=502) @app.post("/api/chat/stream") async def chat_stream_endpoint(request: Request): body = await request.json() message = (body.get("message") or "").strip() if not message: return JSONResponse({"error": "message 不能为空"}, status_code=400) def generate(): try: for piece in chat_stream([{"role": "user", "content": message}]): yield f"data: {piece}\n\n" except Exception as e: yield f"data: [ERROR] {e}\n\n" yield "data: [DONE]\n\n" return StreamingResponse( generate(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, )

启动命令:

uvicorn main:app --host 0.0.0.0 --port 8000 --reload

这里的设计取舍值得说清楚。把client放在模块级只初始化一次,避免每个请求都新建连接;chat_sync和chat_stream分开,是因为批量任务和对话场景对延迟的容忍度完全不同;路由层只做参数校验和异常包装,不写任何模型细节,这样以后加会话历史、加系统提示词,都改在调用层而不是散落在路由里。流式接口用 SSE 而不是 WebSocket,是因为它基于普通 HTTP、单向推送,正好匹配“服务端持续吐字”的需求,前端用fetch读流即可,不需要额外协议。

4. 验证请求:从 curl 到浏览器把接口跑通

服务起来后,先打健康检查,确认进程和配置都加载了:

curl http://127.0.0.1:8000/health

正常返回类似{"status":"ok","model":"你的模型ID"}。如果这里就报错,说明.env没被读到或依赖没装全,先解决再往下走。

接着测非流式对话:

curl -X POST http://127.0.0.1:8000/api/chat \ -H "Content-Type: application/json" \ -d '{"message":"用一句话解释什么是 FastAPI"}'

成功时返回{"reply":"..."},内容是一段完整回答。这一步能通,说明 Key、Base URL、Model ID 三件套全部正确,SDK 调用链路没问题。

再测流式接口,用-N关闭 curl 缓冲,能肉眼看到逐段输出:

curl -N -X POST http://127.0.0.1:8000/api/chat/stream \ -H "Content-Type: application/json" \ -d '{"message":"数一下 1 到 5"}'

你会看到多行data: ...,最后以data: [DONE]结束。如果所有内容一次性涌出来,通常是中间有代理或 Nginx 开了缓冲,本地直连一般不会。

浏览器验证更直观:打开 http://127.0.0.1:8000/docs,FastAPI 自动生成的 Swagger 文档里可以直接点 “Try it out” 填参数发请求,返回体和状态码一目了然。对零基础读者,这个页面是最好的调试入口,比手写 curl 少踩引号和转义的坑。

想加最小前端的话,在static/index.html里用fetch读流即可,核心逻辑是resp.body.getReader()循环解码,按\n\n切分事件、取data:后面的内容追加到页面。这一步不是必须的,接口通了就说明骨架成立,前端只是展示层。

5. 本篇常见错排查:401、连接失败与 choices 报错

401 Unauthorized / invalid_api_key:九成是 Key 问题。检查.env里LLM_API_KEY有没有多余空格或引号,确认 Key 没有过期或被删除,确认它和LLM_BASE_URL来自同一个平台。改完.env要重启服务,load_dotenv()只在启动时读一次。

Connection error / local proxy failed:这类报错说明请求根本没到服务端。先确认本机网络能正常访问外网,再检查环境变量里有没有残留的代理设置干扰 SDK。代码层面确认base_url写的是https://taotoken.net/api,没有多写/v1或少了协议头。如果是在容器里跑,确认容器网络能出网。

IndexError: list index out of range 或 reading 'choices':说明返回体结构和预期不符,resp.choices是空的。常见原因是 Model ID 写错导致返回了错误对象,或者请求被拦截返回了非标准 JSON。把resp整个打印出来看原始结构,比盯着报错猜快得多。另外max_tokens设得过小、触发内容过滤时也可能出现空 choices。

OAuth / 鉴权方式不匹配:如果你用的是某些需要额外鉴权头的客户端,注意 TaoToken 走的是标准 Bearer Token,OpenAI(api_key=...)会自动加Authorization头,不要手动再塞一遍导致重复。Claude Code、Cline 这类工具接入时,Base URL、Key、Model ID 三件套要填全,缺一项就会在鉴权阶段失败。

流式输出卡住不动:先看服务端日志有没有异常,再确认响应头带了X-Accel-Buffering: no。本地直连还卡的话,检查chat_stream里是否真的传了stream=True,以及循环里对delta.content为None的情况做了判断——有些 chunk 只带角色信息不带内容,直接拼接会报错。

排查顺序建议固定为:健康检查 → 非流式 → 流式 → 前端。每一层单独验证,出错时范围立刻缩小一半,比一上来就调前端高效得多。

6. 下一步:把这套骨架用起来

骨架跑通后,最值得先做的两件事:一是把llm.py里的模型切换做成配置项,用环境变量控制,这样同一套代码能在不同模型间对比效果;二是给/api/chat加上简单的会话历史,用字典按session_id存最近几轮消息,注意按 token 数截断,别等超限了才处理。

需要继续拿 Key 或查接入细节,走 API Keys 页面(https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite)和接入文档(https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite);想先验证模型效果,直接去模型对话(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite)试几句;如果你打算长期做编码类或 Agent 类应用,Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite)更适合按量规划成本。

最后留一个实操建议:把本文的curl命令存成一个smoke-test.sh,每次改完配置先跑一遍,三秒钟就能确认链路是否健康。这个习惯比任何调试技巧都省时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进