前言
最近本地 AI 越来越火,Ollama 可以说是我用过最省心的工具了 ------ 不用复杂配置,拉个模型就能在本地跑 Qwen、Llama 这些主流大模型,对开发者太友好了。
但用久了发现,直接调用 Ollama 原生接口还是有点麻烦:每次都要写重复的请求逻辑,流式响应处理起来又琐碎,异常处理也得自己从头写一遍。索性花了点时间,用 TypeScript 封装了一套完整的工具,把流式对话、普通对话、服务状态检查都包了进去,后续项目里直接就能用。
这篇文章就跟大家聊聊整个封装过程,适合想给项目加本地 AI 功能的前端 / 全栈开发者,或者刚接触 Ollama 的朋友参考。
一、准备工作:先搭好基础环境
动手写代码之前,得先把环境搭好,不然跑不起来可就尴尬了。
1. 技术栈说明
- TypeScript:主要是为了类型安全,调用接口的时候能少踩很多类型错误的坑,写起来也更省心。
- Fetch API:浏览器和 Node.js 都能用,不用额外装请求库,轻量化。
- Ollama API :Ollama 自带的接口,默认服务跑在
http://localhost:11434,不用自己写服务端。 - ReadableStream:实现流式打字机效果的核心,用户体验会比一次性返回好很多。
2. 本地环境配置
- 先装个 Ollama,启动服务就行,官方流程走下来很快。
- 拉个模型试试,比如
ollama pull qwen2.5:7b,装完就能在本地跑了。 - 项目这边用 TypeScript 就行,不管是 Node.js 后端还是前端项目都能兼容。
二、先搭好架子:常量和类型定义
写 TypeScript 项目,我习惯先把常量和类型定义好,后面写业务代码会顺畅很多。
1. 服务地址和默认模型
先定义一下 Ollama 的基础地址和默认模型,这样后面修改起来也方便:
ts
// ollama-service.ts
const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || "http://localhost:11434";
const DEFAULT_MODEL = process.env.OLLAMA_MODEL || "qwen2.5:7b";
这里用了环境变量兜底,本地开发默认用 localhost,部署的时候可以通过环境变量改成自定义地址,灵活性更高。
2. 核心类型定义
类型定义是这一套工具的灵魂,能帮我们在写代码的时候就避免很多低级错误:
ts
// 对话消息结构体,对应 Ollama 的消息格式
export interface ChatMessage {
role: "system" | "user" | "assistant";
content: string;
}
// 流式调用的配置项,把回调都包进来,调用的时候更清晰
export interface StreamOptions {
model?: string; // 可选,不填就用默认模型
messages: ChatMessage[]; // 对话上下文,必须传
onToken: (token: string) => void; // 每个 token 输出时的回调,用来做打字机效果
onComplete: (fullResponse: string) => void; // 全部响应完成后的回调
onError: (error: Error) => void; // 出错时的回调,统一处理异常
}
ChatMessage 完全对应 Ollama 的消息格式,role 固定了三种角色,避免传错值。StreamOptions 把流式对话需要的回调都收了进来,调用的时候参数一目了然。
三、核心功能一:流式对话,实现打字机效果
这是我封装这套工具的主要原因 ------ 直接用原生接口处理流式响应太麻烦了,每次都要自己处理流、解析 JSON,索性直接封装成一个方法,用起来就清爽多了。
ts
export async function streamChat(options: StreamOptions): Promise<void> {
// 解构参数,设置默认模型
const { model = DEFAULT_MODEL, messages, onToken, onComplete, onError } = options;
try {
// 1. 发送 POST 请求到 Ollama 的聊天接口
const response = await fetch(`${OLLAMA_BASE_URL}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
messages,
stream: true, // 关键参数:开启流式响应
}),
});
// 2. 先检查请求是否成功
if (!response.ok) {
throw new Error(`Ollama 请求失败: ${response.status}`);
}
// 3. 获取响应流的读取器,准备逐段读取数据
const reader = response.body?.getReader();
if (!reader) {
throw new Error("无法获取响应流,可能是浏览器不支持 ReadableStream");
}
// 4. 准备文本解码器,把二进制数据转成字符串
const decoder = new TextDecoder();
let fullResponse = "";
// 5. 循环读取流数据,直到读取完毕
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 把二进制数据解码成文本
const chunk = decoder.decode(value, { stream: true });
// Ollama 的流式响应是按行返回 JSON 的,所以按行分割
const lines = chunk.split("\n").filter((line) => line.trim());
// 6. 逐行解析 JSON 数据
for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.message?.content) {
const token = data.message.content;
fullResponse += token;
onToken(token); // 每拿到一个 token 就触发回调,前端就能逐字显示了
}
} catch {
// 解析出错的行直接跳过,避免脏数据导致整个程序崩溃
}
}
}
// 7. 所有数据读取完成,触发完成回调,把完整响应传出去
onComplete(fullResponse);
} catch (error) {
// 统一捕获所有异常,传给 onError 回调处理
onError(error instanceof Error ? error : new Error("未知错误"));
}
}
写这段代码的时候踩过的坑
- Ollama 的流式响应格式:每一行都是一个独立的 JSON 对象,必须按行分割再解析,不然会解析失败。
- 脏数据处理 :有时候流里会夹杂一些不规范的行,直接
try/catch跳过就行,别让它影响主流程。 - 浏览器兼容性 :老版本浏览器可能不支持
ReadableStream,所以加了个判断,提前抛出错误提示。
四、核心功能二:非流式对话,简单场景够用了
不是所有场景都需要流式响应,比如后端接口、脚本工具,一次性拿到结果更方便。所以也封装了一个普通的对话方法:
ts
export async function chat(
messages: ChatMessage[],
model = DEFAULT_MODEL
): Promise<string> {
const response = await fetch(`${OLLAMA_BASE_URL}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
messages,
stream: false, // 关闭流式响应,等待完整结果
}),
});
if (!response.ok) {
throw new Error(`Ollama 请求失败: ${response.status}`);
}
const data = await response.json();
return data.message?.content || "";
}
这个就简单多了,关闭 stream 参数,Ollama 会等生成完再一次性返回结果,直接解析 JSON 拿内容就行。
五、核心功能三:先检查服务状态,避免白忙活
很多用户用的时候会忘了启动 Ollama 服务,直接调用接口就会报错。所以加了个状态检查的方法,先看看服务有没有启动,有哪些可用模型,用户体验会好很多:
ts
export async function checkOllamaStatus(): Promise<{ running: boolean; models: string[] }> {
try {
// 调用 Ollama 的模型列表接口,服务正常的话会返回已下载的模型
const response = await fetch(`${OLLAMA_BASE_URL}/api/tags`);
if (!response.ok) {
return { running: false, models: [] };
}
const data = await response.json();
return {
running: true,
models: data.models?.map((m: { name: string }) => m.name) || [],
};
} catch {
// 请求失败说明服务没启动,直接返回 false
return { running: false, models: [] };
}
}
这个方法可以在项目初始化的时候调用,提前告诉用户服务有没有启动,有哪些模型能用,不用等用户调用对话接口才报错。
六、实际用起来,真的很省心
封装完之后,调用起来就几行代码,比直接写原生接口清爽多了。
1. 流式对话示例
比如在命令行里实现逐字输出:
ts
import { streamChat, ChatMessage } from './ollama-service';
const messages: ChatMessage[] = [
{ role: "system", content: "你是一个专业的前端开发助手,回答尽量简洁明了" },
{ role: "user", content: "请解释一下 TypeScript 接口的作用" }
];
streamChat({
messages,
onToken: (token) => {
process.stdout.write(token); // 命令行里逐字打印,模拟打字机效果
},
onComplete: (text) => {
console.log("\n\n完整回答:", text);
},
onError: (err) => {
console.error("出错了:", err.message);
}
});
2. 检查服务状态示例
项目启动前先检查一下:
ts
import { checkOllamaStatus } from './ollama-service';
async function init() {
const status = await checkOllamaStatus();
if (status.running) {
console.log("✅ Ollama 服务已启动");
console.log("可用模型:", status.models.join(", "));
} else {
console.log("❌ Ollama 服务未启动,请先启动 Ollama 再运行程序");
}
}
init();
七、这套封装的小设计,我自己还挺满意的
写的时候没追求花里胡哨,主要是为了自己用着方便,有几个点还挺实用的:
- 配置灵活:支持环境变量自定义服务地址和模型,本地开发和部署都能用。
- 异常处理完善:从请求到流解析,每一步都加了异常处理,不会随便崩。
- 类型安全:全链路 TypeScript 类型定义,写代码的时候 IDE 直接给提示,少踩很多坑。
- 职责单一:流式、非流式、状态检查分开写,哪个出问题改哪个,维护起来也方便。
八、踩过的坑,给大家提个醒
- 服务连接失败 :先检查 Ollama 有没有启动,地址是不是
http://localhost:11434,有时候代理或者防火墙会拦截请求。 - 模型不存在 :记得先
ollama pull把模型拉下来,不然调用的时候会报错。 - 流式响应不生效 :确认
stream: true有没有写对,老版本浏览器可能不支持ReadableStream。
九、写在最后
折腾完这套工具,感觉本地 AI 开发的门槛又低了不少。Ollama 本身已经够省心了,加上 TypeScript 封装,后续在项目里加 AI 功能就像调用普通接口一样简单。
这套代码我自己在好几个小项目里都用上了,没出什么大问题,大家如果需要的话可以直接拿去用,有什么问题或者改进的想法也可以一起聊聊。