1. NestJS 接入 LangChain 实战指南
作为一名从前端转向AI开发的技术人员,我最近在项目中成功将LangChain集成到了NestJS框架中。这个过程让我深刻体会到,选择合适的模型调用方式对应用性能和用户体验有着决定性影响。本文将详细介绍三种核心调用方式:invoke、stream和batch,并分享我在实际开发中的经验教训。
LangChain作为当前最流行的AI应用开发框架之一,为开发者提供了便捷的大模型接入能力。而NestJS作为Node.js领域的企业级框架,其模块化设计和依赖注入特性非常适合构建复杂的AI应用。两者的结合,能够让我们快速搭建出功能完善、性能优越的AI服务。
2. 环境准备与基础配置
2.1 项目初始化与依赖安装
首先,我们需要创建一个新的NestJS项目(如果尚未创建):
bash复制npm i -g @nestjs/cli
nest new langchain-demo
cd langchain-demo
然后安装必要的依赖:
bash复制pnpm install langchain @langchain/openai @langchain/core @nestjs/config
这些依赖包各自承担着重要角色:
langchain:提供LangChain的核心功能@langchain/openai:包含ChatOpenAI等常用模型@langchain/core:LangChain的基础能力@nestjs/config:用于管理环境变量
提示:在实际项目中,我建议使用pnpm而不是npm,因为它能更好地处理依赖关系,特别是在大型项目中可以显著减少node_modules的体积。
2.2 环境变量配置
在项目根目录创建.env文件:
env复制CHAT_BASE_URL=https://api.deepseek.com
CHAT_MODE_NAME=deepseek-chat
CHAT_API_KEY=your-api-key
各参数说明:
CHAT_BASE_URL:指向你的模型服务地址CHAT_MODE_NAME:指定要使用的模型名称CHAT_API_KEY:API访问密钥
安全注意事项:
- 务必在
.gitignore中添加.env,避免敏感信息泄露 - API密钥应该定期轮换
- 不同环境(开发、测试、生产)应该使用不同的密钥
2.3 NestJS配置模块设置
修改app.module.ts以加载环境变量:
typescript复制import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ChatModule } from './chat/chat.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env', `.env.${process.env.NODE_ENV}`]
}),
ChatModule,
],
})
export class AppModule {}
这样配置后,我们可以在应用的任何地方通过process.env访问环境变量。envFilePath的配置允许我们根据不同的环境(development、production等)加载不同的环境变量文件。
3. LangChain模型初始化
3.1 创建ChatService
在src/chat目录下创建chat.service.ts:
typescript复制import { Injectable } from '@nestjs/common';
import { ChatOpenAI } from '@langchain/openai';
@Injectable()
export class ChatService {
private model: ChatOpenAI;
constructor() {
this.model = new ChatOpenAI({
configuration: {
baseURL: process.env.CHAT_BASE_URL,
},
apiKey: process.env.CHAT_API_KEY,
model: process.env.CHAT_MODE_NAME,
temperature: 0.7,
maxTokens: 2048,
timeout: 60000,
maxRetries: 3
});
}
}
关键参数解析:
temperature(0-1):控制输出的随机性。值越高,输出越有创意但可能偏离预期;值越低,输出越确定但可能缺乏变化。maxTokens:限制模型输出的最大token数量,需要根据模型的最大上下文长度合理设置。timeout:请求超时时间(毫秒),根据网络状况和模型响应时间调整。maxRetries:失败重试次数,建议设置为3次以应对临时性网络问题。
3.2 模型配置优化建议
在实际项目中,我发现以下配置策略很有效:
- 对于问答类应用,temperature设为0.3-0.5可以获得更准确的回答
- 对于创意生成类应用,temperature可以提高到0.7-0.9
- 超时时间应根据API的实际响应时间设置,通常1-2分钟比较合适
- 对于付费API,合理设置maxTokens可以控制成本
4. 三种调用方式详解
4.1 invoke同步调用
4.1.1 实现原理
invoke是最基础的调用方式,它会等待模型完全生成响应后一次性返回。这种方式实现简单,适合不需要实时交互的场景。
typescript复制async invoke(text: string) {
const result = await this.model.invoke(text);
return result.content;
}
4.1.2 控制器实现
typescript复制@Get(':text')
invoke(@Param('text') text: string) {
return this.chatService.invoke(text);
}
4.1.3 使用场景与限制
适用场景:
- 简单问答
- 文本分类
- 实体识别
限制:
- 响应时间取决于生成的全部内容长度
- 无法实现实时交互体验
- 长文本生成可能导致客户端超时
经验分享:在早期版本中,我们没有设置客户端超时时间,导致某些长响应请求挂起。后来我们统一设置了30秒的客户端超时,并添加了友好的超时提示。
4.2 stream流式调用
4.2.1 流式调用优势
stream方式让模型可以逐token返回结果,这种方式的优势在于:
- 用户可以获得即时反馈
- 降低感知延迟
- 适合长文本生成场景
4.2.2 服务层实现
typescript复制async stream(text: string) {
return this.model.stream(text);
}
4.2.3 控制器与SSE实现
typescript复制@Get('stream/:text')
async stream(@Param('text') text: string, @Res() res: Response) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = await this.chatService.stream(text);
for await (const chunk of stream) {
if (!chunk.content) continue;
res.write(`data: ${JSON.stringify({ content: chunk.content })}\n\n`);
}
res.end();
}
4.2.4 前端集成示例
javascript复制const eventSource = new EventSource('/chat/stream/你好');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(data.content);
};
eventSource.onerror = (error) => {
console.error('EventSource failed:', error);
eventSource.close();
};
踩坑记录:初期我们遇到了中文乱码问题,后来发现需要在响应头中添加
charset=utf-8。另外,某些浏览器对SSE连接数有限制,同一域名下最多只能有6个连接,这在设计多标签应用时需要特别注意。
4.3 batch批量调用
4.3.1 批量处理优势
batch方式允许我们一次性发送多个请求,这对于批量处理任务非常高效:
- 减少网络往返时间
- 提高整体吞吐量
- 简化客户端逻辑
4.3.2 服务层实现
typescript复制async batch(texts: string) {
const inputs = texts.split(',');
const result = await this.model.batch(inputs);
return {
inputs,
outputs: result.map(message => message.content)
};
}
4.3.3 控制器实现
typescript复制@Get('batch/:texts')
batch(@Param('texts') texts: string) {
return this.chatService.batch(texts);
}
4.3.4 性能优化建议
- 合理设置批量大小:根据API限制和内存情况,通常10-20个为一组比较合适
- 错误处理:批量中单个失败不应影响其他请求
- 进度反馈:长时间批量处理应提供进度反馈机制
5. 高级应用与优化
5.1 自定义提示模板
LangChain的强大之处在于可以灵活定义提示模板:
typescript复制import { PromptTemplate } from 'langchain/prompts';
const template = "你是一个专业的{role},请用{style}风格回答以下问题:{question}";
const prompt = new PromptTemplate({
template,
inputVariables: ["role", "style", "question"]
});
// 使用
const formattedPrompt = await prompt.format({
role: "医生",
style: "专业且友好",
question: "感冒了怎么办?"
});
5.2 记忆与上下文管理
对于聊天应用,保持对话上下文至关重要:
typescript复制import { BufferMemory } from 'langchain/memory';
const memory = new BufferMemory({
memoryKey: "chat_history",
returnMessages: true
});
// 在对话中保存上下文
await memory.saveContext(
{ input: "你好" },
{ output: "你好!有什么可以帮你的?" }
);
// 获取历史记录
const history = await memory.loadMemoryVariables({});
5.3 链式调用
LangChain的链式调用可以构建复杂的工作流:
typescript复制import { LLMChain } from 'langchain/chains';
const chain = new LLMChain({
llm: this.model,
prompt: prompt,
memory: memory
});
const result = await chain.call({
role: "医生",
style: "专业且友好",
question: "我头痛应该吃什么药?"
});
6. 性能监控与错误处理
6.1 添加日志记录
typescript复制async invoke(text: string) {
const start = Date.now();
try {
const result = await this.model.invoke(text);
const duration = Date.now() - start;
this.logger.log(`Invoke成功: ${duration}ms`);
return result.content;
} catch (error) {
this.logger.error(`Invoke失败: ${error.message}`);
throw new Error('处理请求时出错');
}
}
6.2 实现重试机制
typescript复制async invokeWithRetry(text: string, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await this.model.invoke(text);
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
6.3 限流与熔断
在高并发场景下,建议实现限流:
typescript复制import { RateLimiterMemory } from 'rate-limiter-flexible';
const rateLimiter = new RateLimiterMemory({
points: 10, // 10次请求
duration: 1 // 每秒
});
async invokeLimited(text: string) {
try {
await rateLimiter.consume('invoke');
return this.invoke(text);
} catch (limiterError) {
throw new Error('请求过于频繁,请稍后再试');
}
}
7. 部署与生产环境建议
7.1 容器化部署
使用Docker可以简化部署:
dockerfile复制FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "dist/main.js"]
7.2 健康检查
添加健康检查端点:
typescript复制@Get('health')
healthCheck() {
return {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime()
};
}
7.3 性能优化
- 启用gzip压缩
- 使用缓存中间件
- 考虑使用Redis存储会话数据
- 实现连接池管理
8. 三种调用方式对比与选型
| 特性 | invoke | stream | batch |
|---|---|---|---|
| 响应方式 | 一次性返回 | 流式返回 | 批量返回 |
| 延迟感知 | 高 | 低 | 中等 |
| 实现复杂度 | 简单 | 中等 | 中等 |
| 适用场景 | 简单问答 | 实时聊天 | 批量处理 |
| 资源占用 | 中等 | 低-中等 | 高 |
| 错误处理 | 简单 | 复杂 | 中等 |
选型建议:
- 面向最终用户的交互式应用优先考虑stream方式
- 后台批量处理任务使用batch方式更高效
- 简单的API接口可以使用invoke简化实现
在实际项目中,我们最终采用了混合策略:主要交互使用stream,后台任务使用batch,管理接口使用invoke。这种组合在保证用户体验的同时,也兼顾了系统效率。