前言
最近我尝试用 Bun 写了一个调用 DeepSeek API 的小 demo。
严格来说,这不是一个复杂项目,更像是一次从 0 到 1 调通接口的练习。过程中我顺便理解了 Bun 怎么运行 TypeScript、Axios 怎么发送 POST 请求、API Key 应该怎么放到 .env 里,以及一个大模型 API 请求大概由哪些部分组成。
这篇文章主要记录整个流程,也适合和我一样刚接触 Bun、Axios、大模型 API 调用的同学参考。
这个 demo 做了什么
先用一句话概括:
使用 Bun 运行 TypeScript 代码,通过 Axios 向 DeepSeek API 发送请求,然后把大模型返回的内容打印到终端。
整体流程大概是这样:
javascript
用户输入问题
↓
Axios 发送 HTTP POST 请求
↓
请求带着 API Key 到 DeepSeek 服务器
↓
DeepSeek 模型生成回答
↓
服务器返回 JSON 数据
↓
程序从 JSON 中取出回答并打印
所以这个 demo 并不是在本地运行大模型,也不是训练模型,而是调用 DeepSeek 提供好的 API 服务。
Bun 是什么
一句话:Bun 是一个新的 JavaScript 运行时,也可以看作 Node.js 的替代方案之一。
它不只是运行时,还内置了包管理、打包、测试等能力。
几个比较实用的特点:
- 速度快:Bun 使用 Zig 编写,底层使用 JavaScriptCore,启动和安装依赖都比较快
- 原生运行 TypeScript :不用额外安装
ts-node,可以直接运行.ts文件 - 内置包管理器 :可以用
bun install、bun add管理依赖 - 自动读取
.env:一般情况下不需要额外安装dotenv - 自带测试工具 :可以用
bun test跑测试
对于新手来说,最直观的感受就是:很多原来需要单独配置的东西,Bun 已经帮我们内置好了。
初始化项目
创建项目目录:
bash
mkdir axios-demo
cd axios-demo
bun init
初始化过程中可以一路回车,先使用默认配置即可。
项目结构大概是这样:
bash
axios-demo/
├── index.ts
├── package.json
├── tsconfig.json
├── .env
├── .gitignore
└── bun.lock
其中比较重要的是:
bash
index.ts 写主要代码
package.json 记录项目依赖和脚本
tsconfig.json TypeScript 配置
.env 存放环境变量,比如 API Key
.gitignore 配置哪些文件不提交到 Git 仓库
bun.lock Bun 生成的依赖锁定文件
安装依赖
这个 demo 只需要安装 Axios:
csharp
bun add axios
如果项目里还没有 Bun 的类型提示,也可以安装:
sql
bun add -d @types/bun
Bun 会自动加载
.env文件,所以这里不需要安装dotenv,也不需要手动写dotenv.config()。
安装完成后,package.json 大概类似这样:
json
{
"name": "axios-demo",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"axios": "^1.17.0"
}
}
这里的版本号不一定完全一样,以自己实际安装出来的为准。
TypeScript 配置
Bun 初始化项目时通常会生成一份 tsconfig.json。
我的配置大概如下:
json
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"types": ["bun"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
}
}
其中我目前主要理解这三个配置:
json
"module": "Preserve"
表示保留源码里的模块语法,不急着转换,交给 Bun 去处理。
json
"types": ["bun"]
表示让 TypeScript 和编辑器识别 Bun 提供的内置 API。
json
"noEmit": true
表示 TypeScript 不需要额外输出 JavaScript 文件,因为 Bun 可以直接运行 .ts 文件。
配置环境变量
在项目根目录下创建 .env 文件:
ini
DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
DEEPSEEK_API_KEY=sk-your-api-key-here
这里有两个变量:
vbnet
DEEPSEEK_API_URL DeepSeek 的接口地址
DEEPSEEK_API_KEY 自己的 API Key
注意:.env 文件里存的是敏感信息,尤其是 API Key。
所以一定要在 .gitignore 里加上:
bash
.env
否则如果不小心把 API Key 提交到 GitHub 或 Gitee,别人就可能拿你的 Key 去调用接口,甚至消耗你的额度。
编写代码
核心代码写在 index.ts 里:
javascript
import axios from "axios";
const apiURL = process.env.DEEPSEEK_API_URL;
const apiKey = process.env.DEEPSEEK_API_KEY;
if (!apiURL) {
throw new Error("DEEPSEEK_API_URL 没有配置");
}
if (!apiKey) {
throw new Error("DEEPSEEK_API_KEY 没有配置");
}
async function chat() {
try {
const res = await axios.post(
apiURL,
{
model: "deepseek-v4-flash",
messages: [
{
role: "user",
content: "你好,介绍一下 Bun",
},
],
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
}
);
console.log(res.data.choices[0].message.content);
} catch (err: any) {
console.error("请求失败:", err.message);
if (err.response) {
console.error("HTTP 状态码:", err.response.status);
console.error("错误详情:", err.response.data);
}
}
}
chat();
运行:
arduino
bun run index.ts
也可以直接:
bun index.ts
如果配置没问题,终端里应该就能看到 DeepSeek 返回的回答。
代码拆解
一开始我只是把代码跑通了,但后来回头看,其实这段代码主要分成几部分。
1. 读取环境变量
ini
const apiURL = process.env.DEEPSEEK_API_URL;
const apiKey = process.env.DEEPSEEK_API_KEY;
这两行代码是从 .env 文件里读取配置。
因为 Bun 会自动加载 .env,所以我们可以直接通过 process.env 拿到对应的值。
2. 判断配置是否存在
javascript
if (!apiURL) {
throw new Error("DEEPSEEK_API_URL 没有配置");
}
if (!apiKey) {
throw new Error("DEEPSEEK_API_KEY 没有配置");
}
如果没有配置接口地址或者 API Key,程序就直接报错。
这样做的好处是:
问题能尽早暴露,不然请求发出去之后再失败,排查起来更麻烦。
3. 使用 axios.post 发送请求
php
const res = await axios.post(
apiURL,
{
model: "deepseek-v4-flash",
messages: [
{
role: "user",
content: "你好,介绍一下 Bun",
},
],
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
}
);
axios.post() 这里有三个主要参数:
css
第一个参数:请求地址
第二个参数:请求体 body
第三个参数:请求配置,比如 headers
对应到这段代码就是:
apiURL
表示请求要发到哪里。
css
{
model: "deepseek-v4-flash",
messages: [
{
role: "user",
content: "你好,介绍一下 Bun",
},
],
}
表示要发送给 DeepSeek 的数据。
css
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
}
表示请求头,告诉服务器数据格式是什么,以及我是谁。
4. 打印模型返回内容
ini
console.log(res.data.choices[0].message.content);
DeepSeek 返回的不是一段普通字符串,而是一份 JSON 数据。
我们要从这份 JSON 里面取出模型真正回复的内容。
可以简单理解成:
css
res
↓
data
↓
choices[0]
↓
message
↓
content
最后拿到的 content 才是模型返回的文字。
三个必知的基础知识
1. 为什么这里用 POST,而不是 GET
像 Chat Completions 这种接口,需要把 model、messages 等数据放到请求体里发送,所以一般使用 POST。
GET 和 POST 可以简单对比一下:
| 维度 | GET | POST |
|---|---|---|
| 常见用途 | 获取数据 | 提交数据 |
| 参数位置 | 通常放在 URL 上 | 通常放在请求体 body 里 |
| 数据量 | 受 URL 长度限制 | 更适合提交较大的 JSON |
| 敏感信息 | 不适合放在 URL 里 | 一般放在请求头或请求体里 |
在这个 demo 里,messages 可能会很长,还需要传 model 等参数,所以用 POST 更合适。
需要注意的是,不是所有大模型相关接口都必须用 POST。
比如有些"查询模型列表"的接口就可能使用 GET。
所以实际开发时,还是要以接口文档为准。
2. HTTP 请求大概由哪几部分组成
一个 HTTP 请求可以先简单理解成三部分:
请求行:请求方法 + 请求路径 + 协议版本
请求头:一些额外信息,比如数据格式、认证信息
请求体:真正提交的数据
对应到本次请求,大概是:
bash
请求行:
POST /chat/completions HTTP/1.1
请求头:
Content-Type: application/json
Authorization: Bearer sk-xxx
请求体:
{
"model": "deepseek-v4-flash",
"messages": [
{
"role": "user",
"content": "你好,介绍一下 Bun"
}
]
}
理解这三部分以后,再去看其他 HTTP API,会清楚很多。
比如:
javascript
接口地址放哪里?
认证信息放哪里?
JSON 数据放哪里?
这些问题就能慢慢对应起来。
3. Axios 错误处理为什么比较方便
原生 fetch 在遇到 400、500 这类 HTTP 状态码时,不一定会直接进入 catch,通常需要自己判断 response.ok。
而 Axios 默认情况下,只要状态码不在 2xx 范围内,就会进入 catch。
所以我们可以这样处理错误:
typescript
try {
const res = await axios.post(url, data, config);
console.log(res.data);
} catch (err: any) {
console.error("请求失败:", err.message);
if (err.response) {
console.error("HTTP 状态码:", err.response.status);
console.error("错误详情:", err.response.data);
}
}
这对于调试 API 很方便。
比如:
vbnet
401:可能是 API Key 错了
404:可能是接口地址写错了
429:可能是请求太频繁或额度限制
500:可能是服务端内部错误
出错以后,能看到状态码和错误详情,排查起来会轻松很多。
Bun 常用命令速查
| 场景 | Node.js / npm | Bun |
|---|---|---|
| 初始化项目 | npm init |
bun init |
| 安装依赖 | npm install |
bun install |
| 添加依赖 | npm install axios |
bun add axios |
| 添加开发依赖 | npm install -D xxx |
bun add -d xxx |
| 运行 TS 文件 | npx ts-node index.ts |
bun index.ts |
| 运行脚本 | npm run dev |
bun run dev |
| 临时执行命令 | npx eslint . |
bunx eslint . |
| 运行测试 | npx jest |
bun test |
加载 .env |
通常需要 dotenv |
自动加载 |
Bun 还有哪些内置能力
这次 demo 里主要用到了 Bun 运行 TypeScript 和自动加载 .env 的能力。
除此之外,Bun 还内置了一些常见功能。
下面这些代码主要是为了了解 Bun 的能力范围,不是本篇 demo 的必要代码。
javascript
// HTTP 服务器
Bun.serve({
port: 3000,
fetch(req) {
return new Response("Hello Bun!");
},
});
// SQLite
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");
// 文件操作
const file = Bun.file("./data.json");
const data = await file.json();
// Shell 命令
const result = await Bun.$`ls -la`.text();
也就是说,Bun 不只是能运行 JavaScript / TypeScript,它还想把很多常用开发能力都集成进来。
对于小项目或者练习项目来说,这种"一套工具完成很多事"的体验还是挺方便的。
常见问题排查
1. 提示 API Key 没有配置
先检查 .env 文件是不是放在项目根目录。
正确位置应该是:
bash
axios-demo/
├── index.ts
├── package.json
└── .env
再检查变量名是否和代码里一致:
ini
DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
DEEPSEEK_API_KEY=sk-your-api-key-here
代码里也要对应:
arduino
process.env.DEEPSEEK_API_URL
process.env.DEEPSEEK_API_KEY
2. 提示 401
一般是认证失败。
常见原因:
vbnet
API Key 写错了
API Key 前后多了空格
Authorization 格式写错了
Key 已经过期或被禁用
请求头应该类似这样:
javascript
Authorization: `Bearer ${apiKey}`
3. 提示模型不存在
如果返回类似"model not found"的错误,优先检查 model 字段。
vbnet
model: "deepseek-v4-flash"
模型名称可能会随着平台调整而变化,所以实际项目里建议以官方文档或控制台显示为准。
总结
这个 demo 虽然很小,但对我来说还是挺有收获的。
它让我把下面几个知识点串起来了:
- 用 Bun 初始化和运行 TypeScript 项目
- 用 Axios 发送 HTTP POST 请求
- 把 API Key 放到
.env里,而不是直接写死在代码中 - 理解 HTTP 请求里的请求头和请求体
- 知道大模型接口返回的是 JSON,需要从里面取出真正的回复内容
- 学会通过状态码和错误信息排查问题
最后再总结一下这次请求的核心流程:
javascript
Bun 运行 index.ts
↓
读取 .env 中的 API 地址和 API Key
↓
Axios 发送 POST 请求
↓
DeepSeek 返回 JSON
↓
从 JSON 中取出 message.content
↓
打印到终端
整体来看,Bun 的上手成本确实比较低,尤其是能直接运行 TypeScript、自动加载 .env,对新手很友好。
这次只是调通了一个最小 demo,后面如果继续扩展,可以尝试把它改成一个简单的 Web 页面,做到:
页面输入问题
↓
后端调用 DeepSeek API
↓
页面显示模型回答
这样就能从"命令行 demo"慢慢过渡到一个真正的小项目。