从零封装 Ollama AI 服务:TypeScript 流式对话工具开发

前言

最近本地 AI 越来越火,Ollama 可以说是我用过最省心的工具了 ------ 不用复杂配置,拉个模型就能在本地跑 Qwen、Llama 这些主流大模型,对开发者太友好了。

但用久了发现,直接调用 Ollama 原生接口还是有点麻烦:每次都要写重复的请求逻辑,流式响应处理起来又琐碎,异常处理也得自己从头写一遍。索性花了点时间,用 TypeScript 封装了一套完整的工具,把流式对话、普通对话、服务状态检查都包了进去,后续项目里直接就能用。

这篇文章就跟大家聊聊整个封装过程,适合想给项目加本地 AI 功能的前端 / 全栈开发者,或者刚接触 Ollama 的朋友参考。


一、准备工作:先搭好基础环境

动手写代码之前,得先把环境搭好,不然跑不起来可就尴尬了。

1. 技术栈说明

  • TypeScript:主要是为了类型安全,调用接口的时候能少踩很多类型错误的坑,写起来也更省心。
  • Fetch API:浏览器和 Node.js 都能用,不用额外装请求库,轻量化。
  • Ollama API :Ollama 自带的接口,默认服务跑在 http://localhost:11434,不用自己写服务端。
  • ReadableStream:实现流式打字机效果的核心,用户体验会比一次性返回好很多。

2. 本地环境配置

  1. 先装个 Ollama,启动服务就行,官方流程走下来很快。
  2. 拉个模型试试,比如 ollama pull qwen2.5:7b,装完就能在本地跑了。
  3. 项目这边用 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("未知错误"));
  }
}

写这段代码的时候踩过的坑

  1. Ollama 的流式响应格式:每一行都是一个独立的 JSON 对象,必须按行分割再解析,不然会解析失败。
  2. 脏数据处理 :有时候流里会夹杂一些不规范的行,直接 try/catch 跳过就行,别让它影响主流程。
  3. 浏览器兼容性 :老版本浏览器可能不支持 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 直接给提示,少踩很多坑。
  • 职责单一:流式、非流式、状态检查分开写,哪个出问题改哪个,维护起来也方便。

八、踩过的坑,给大家提个醒

  1. 服务连接失败 :先检查 Ollama 有没有启动,地址是不是 http://localhost:11434,有时候代理或者防火墙会拦截请求。
  2. 模型不存在 :记得先 ollama pull 把模型拉下来,不然调用的时候会报错。
  3. 流式响应不生效 :确认 stream: true 有没有写对,老版本浏览器可能不支持 ReadableStream

九、写在最后

折腾完这套工具,感觉本地 AI 开发的门槛又低了不少。Ollama 本身已经够省心了,加上 TypeScript 封装,后续在项目里加 AI 功能就像调用普通接口一样简单。

这套代码我自己在好几个小项目里都用上了,没出什么大问题,大家如果需要的话可以直接拿去用,有什么问题或者改进的想法也可以一起聊聊。

相关推荐
ZengLiangYi13 小时前
MCP 协议从零实现:手写最简 MCP Server
前端·javascript·后端
罗超驿13 小时前
2.HTML表格详解:标签、属性与单元格合并实战
前端·html
李子琪。13 小时前
Web 漏洞实战全解析:CSRF 攻击原理、Token 防御机制与实验验证(上)
前端·网络·经验分享·csrf
小救星小杜、13 小时前
new Router base的作用
前端·javascript·vue.js
程序二次开发13 小时前
wordpress 文章页,文章分类,单页,woocommerc 产品页,分类页添加.html后缀
大数据·前端·html·php
CodeSheep13 小时前
苦撑13年,创始人离职出走,拉勾终究还是倒下了…
前端·后端·程序员
a11177613 小时前
html制作的PPT(各种风格)提示词
前端·开源·html
cvcode_study13 小时前
Electron 制作自定义浏览器
前端·javascript·electron
JCJC错别字检测-田春峰13 小时前
字根秀秀 HTML 托管现已支持“用户登录”功能,一键变身 Web App!
前端·html·web app·网页托管