在 AI Agent 服务端开发中,将本地大模型封装成稳定、可复用的业务接口是常见需求。借助 NestJS 的模块化结构、LangChain 的模型调用能力和 Ollama 的本地运行环境,可以快速搭建一套基础对话服务。下面从项目初始化开始,逐步完成配置、接口实现、参数校验与故障排查。
bash
# 启动服务(默认 http://127.0.0.1:11434)
ollama serve
# 拉取一个模型
ollama pull qwen2.5:7b
# 验证模型可用
curl http://127.0.0.1:11434/api/tags
bash
npm i -g @nestjs/cli
nest new nest-langchain-demo
cd nest-langchain-demo
bash
npm install @langchain/ollama @langchain/core
npm install dotenv
注意:
@langchain/ollama要求 Node.js >= 18,不需要安装langchain大杂烩包,按需引入即可。
.env 文件env
# Ollama 配置
OLLAMA_HOST=http://127.0.0.1:11434
OLLAMA_CHAT_MODEL=qwen2.5:7b
OLLAMA_TEMPERATURE=0.7
src/config.tsTypeScript
import 'dotenv/config';
export const config = {
ollama: {
host: process.env.OLLAMA_HOST || 'http://127.0.0.1:11434',
ch@tModel: process.env.OLLAMA_CHAT_MODEL || 'qwen2.5:7b',
temperature: Number(process.env.OLLAMA_TEMPERATURE ?? 0.7),
},
};
bash
nest g module models
nest g service models
nest g controller models
src/models/models.service.tsTypeScript
import { Injectable, Logger } from '@nestjs/common';
import { config } from '../config';
import { ChatOllama } from '@langchain/ollama';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
@Injectable()
export class ModelsService {
private readonly logger = new Logger(ModelsService.name);
// 创建 LLM 实例(单例,全局复用)
private llm = new ChatOllama({
model: config.ollama.ch@tModel,
temperature: config.ollama.temperature,
baseUrl: config.ollama.host,
think: false, // 关闭思考模式,减少 token 消耗
});
/**
* 普通对话
*/
async baseChat(message: string) {
const response = await this.llm.invoke([new HumanMessage(message)]);
return {
question: message,
answer: response.content,
usageToken: response.usage_metadata, // token 用量统计
};
}
/**
* 角色扮演对话
*/
async ch@tRole(role: string, message: string) {
const response = await this.llm.invoke([
new SystemMessage(role),
new HumanMessage(message),
]);
return {
role,
question: message,
answer: response.content,
usageToken: response.usage_metadata,
};
}
}
src/models/dto/[email protected]TypeScript
import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
export class BaseChatDto {
@IsString()
@IsNotEmpty({ message: 'message 不能为空' })
@MaxLength(4000)
message: string;
}
export class RoleChatDto extends BaseChatDto {
@IsOptional()
@IsString()
@MaxLength(500)
role?: string = '你是一个乐于助人的中文助手';
}
src/models/models.controller.tsTypeScript
import { Body, Controller, Post } from '@nestjs/common';
import { ModelsService } from './models.service';
import { BaseChatDto, RoleChatDto } from './dto/[email protected]';
@Controller('models')
export class ModelsController {
constructor(private readonly modelsService: ModelsService) {}
// 普通对话
@Post('ch@t')
baseChat(@Body() dto: BaseChatDto) {
return this.modelsService.baseChat(dto.message);
}
// 角色对话
@Post('ch@t/role')
ch@tRole(@Body() dto: RoleChatDto) {
return this.modelsService.ch@tRole(dto.role, dto.message);
}
}
src/models/models.module.tsTypeScript
import { Module } from '@nestjs/common';
import { ModelsService } from './models.service';
import { ModelsController } from './models.controller';
@Module({
controllers: [ModelsController],
providers: [ModelsService],
exports: [ModelsService], // 导出后其他模块也能用
})
export class ModelsModule {}
src/app.module.tsTypeScript
import { Module } from '@nestjs/common';
import { ModelsModule } from './models/models.module';
@Module({
imports: [ModelsModule],
})
export class AppModule {}
src/main.tsTypeScript
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({ whitelist: true, transform: true }),
);
app.setGlobalPrefix('api'); // 统一前缀
await app.listen(3000);
console.log('? Server: http://localhost:3000/api');
}
bootstrap();
需要
npm i class-validator class-transformer。
bash
npm run start:dev
普通对话:
bash
curl -X POST http://localhost:3000/api/models/ch@t
-H "Content-Type: application/json"
-d '{"message": "用一句话介绍 NestJS"}'
返回:
JSON
{
"question": "用一句话介绍 NestJS",
"answer": "NestJS 是一个基于 Node.js 的渐进式框架,使用 TypeScript 构建,结合了 OOP、FP 和 FRP 范式。",
"usageToken": {
"input_tokens": 15,
"output_tokens": 28,
"total_tokens": 43
}
}
角色对话:
bash
curl -X POST http://localhost:3000/api/models/ch@t/role
-H "Content-Type: application/json"
-d '{"role": "你是一名资深的 Java 架构师", "message": "讲讲微服务拆分的原则"}'
fetch failed检查 Ollama 是否启动、端口是否正确:
bash
curl http://127.0.0.1:11434 # 应返回 "Ollama is running"
如果是 Docker 部署的 Ollama,baseUrl 不能用 127.0.0.1(容器内回环地址),要用宿主机 IP 或 host.docker.internal。
model 'xxx' not foundbash
ollama pull qwen2.5:7b # 名称必须和 .env 中完全一致
模型第一次加载到内存需要数秒到数十秒,属于正常现象。可在服务启动时预热一次:
TypeScript
async onModuleInit() {
await this.llm.invoke([new HumanMessage('hi')]); // 预热
}
think 参数说明think: false:关闭模型的推理过程输出,只返回最终答案,省 token、速度快think: true:返回思考过程(部分模型支持,如 qwen3、deepseek-r1),response.content 中可能包含 <think>...</think> 标签,需要自行剥离大模型生成较慢时,可在 ChatOllama 配置中增加:
TypeScript
new ChatOllama({
// ...其他配置
timeout: 120000, // 2 分钟
numPredict: 512, // 限制最大生成 token 数,防止无限输出
})
plain
src/
├── config.ts # 配置
├── main.ts # 入口
├── app.module.ts
└── models/
├── dto/
│ └── [email protected] # 参数校验
├── models.module.ts
├── models.controller.ts # 接口层
└── models.service.ts # 业务层(调用 Ollama)