一、结构化输出的核心价值与设计原则
在智能体开发中,结构化输出是连接后端逻辑与前端交互的桥梁。相较于自由文本,结构化数据(如JSON Schema)能显著提升信息解析效率,尤其在多模态交互场景下,清晰的字段定义可避免语义歧义。
1.1 DTO设计规范
NestJS推荐使用数据传输对象(DTO)定义输出结构,例如:
// src/agent/dto/output.dto.tsexport class AgentOutputDto {@ApiProperty({ description: '智能体唯一标识' })agentId: string;@ApiProperty({type: 'object',description: '结构化响应内容',example: {text: '查询成功',data: { temperature: 25, humidity: 60 }}})response: {text: string;data?: Record<string, any>;};@ApiProperty({ description: '工具调用状态' })status: 'success' | 'pending' | 'failed';}
设计要点:
- 字段分级:基础信息(如agentId)与业务数据分离
- 类型安全:通过Zod或class-validator进行运行时校验
- 扩展性:预留
data字段支持动态业务数据
1.2 中间件处理链
通过NestJS中间件实现输出标准化:
// src/common/middleware/output-formatter.middleware.tsexport class OutputFormatterMiddleware implements NestMiddleware {use(req: Request, res: Response, next: NextFunction) {const originalJson = res.json;res.json = (body: any) => {const formatted = {timestamp: new Date().toISOString(),...(this.isAgentResponse(body) ?{ agentOutput: this.normalizeAgentOutput(body) } :body)};return originalJson.call(res, formatted);};next();}private isAgentResponse(body: any): boolean {return body?.hasOwnProperty('agentId');}}
最佳实践:
- 在全局中间件中注入时间戳、请求ID等元数据
- 对特定路由(如
/api/agent/*)应用差异化格式化 - 避免过度嵌套,保持三级以内结构
二、工具调用的异步处理模式
智能体的核心能力往往依赖外部工具(如数据库查询、API调用),需建立可靠的异步处理机制。
2.1 工具注册与发现
采用依赖注入方式管理工具:
// src/tools/tool-registry.service.ts@Injectable()export class ToolRegistryService {private readonly tools = new Map<string, ToolInterface>();registerTool(name: string, tool: ToolInterface) {this.tools.set(name, tool);}getTool(name: string): ToolInterface {const tool = this.tools.get(name);if (!tool) throw new NotFoundException(`Tool ${name} not found`);return tool;}}// src/tools/database.tool.ts@Injectable()export class DatabaseTool implements ToolInterface {constructor(private prisma: PrismaService) {}async execute(query: string): Promise<any> {return this.prisma.$queryRawUnsafe(query);}}
架构优势:
- 集中式管理避免工具重复实例化
- 通过接口抽象实现工具热插拔
- 结合AOP实现调用日志、权限校验等横切关注点
2.2 异步调用编排
使用RxJS处理工具链调用:
// src/agent/services/agent.service.tsexecuteToolChain(tools: string[], input: any): Observable<any> {return from(tools).pipe(concatMap(toolName => {const tool = this.toolRegistry.getTool(toolName);return tool.execute(input).pipe(tap(result => this.logger.log(`Tool ${toolName} executed`)),catchError(err => {this.logger.error(`Tool ${toolName} failed`, err.stack);return throwError(err);}));}),reduce((acc, curr) => ({ ...acc, ...curr }), {}));}
性能优化:
- 对无依赖的工具采用
mergeMap并行执行 - 设置并发限制避免资源耗尽
- 通过
retry策略处理瞬时故障
三、典型场景实现方案
3.1 多轮对话状态管理
结合Redis实现跨请求状态持久化:
// src/agent/services/dialog-manager.service.ts@Injectable()export class DialogManagerService {constructor(@InjectRedis() private readonly redis: Redis,private readonly agentService: AgentService) {}async handleDialog(sessionId: string, input: DialogInputDto) {const dialogState = await this.getDialogState(sessionId);const context = { ...dialogState, ...input };const result = await this.agentService.process(context);await this.updateDialogState(sessionId, {lastInput: input,lastOutput: result,step: dialogState.step + 1});return result;}private async getDialogState(sessionId: string) {const cached = await this.redis.get(`dialog:${sessionId}`);return cached ? JSON.parse(cached) : { step: 0 };}}
关键考虑:
- 设置合理的TTL避免状态堆积
- 对敏感数据加密存储
- 实现状态迁移验证逻辑
3.2 工具调用超时控制
通过AbortController实现:
// src/tools/http-client.tool.ts@Injectable()export class HttpClientTool implements ToolInterface {async execute(config: HttpRequestConfig): Promise<any> {const controller = new AbortController();const timeoutId = setTimeout(() => controller.abort(), 5000);try {const response = await fetch(config.url, {signal: controller.signal,...config});clearTimeout(timeoutId);return response.json();} catch (err) {if (err.name === 'AbortError') {throw new RequestTimeoutException('Tool execution timed out');}throw err;}}}
容错设计:
- 熔断机制:连续超时后自动降级
- 备份工具:主工具失败时切换备用方案
- 监控告警:记录超时率异常波动
四、测试与质量保障
4.1 结构化输出验证
使用Fastify的JSON Schema验证:
// src/agent/agent.controller.ts@Post()@UsePipes(new ValidationPipe({ transform: true }))@UseInterceptors(new SchemaValidatorInterceptor({schema: {type: 'object',properties: {agentOutput: {type: 'object',required: ['text', 'status'],properties: {text: { type: 'string' },status: { enum: ['success', 'pending', 'failed'] }}}}}}))async handleRequest(@Body() input: AgentInputDto) {// ...}
4.2 工具调用模拟测试
创建工具存根(Stub)进行隔离测试:
// test/tools/database.tool.stub.tsexport class DatabaseToolStub implements ToolInterface {private mockData: Record<string, any> = {};setMockData(key: string, value: any) {this.mockData[key] = value;}async execute(query: string): Promise<any> {const match = query.match(/SELECT\s+\*\s+FROM\s+(\w+)/i);if (match) return this.mockData[match[1]] || [];throw new Error('Query not mocked');}}// 在测试中替换真实工具beforeEach(() => {const mock = new DatabaseToolStub();mock.setMockData('users', [{ id: 1, name: 'Test' }]);const module = await Test.createTestingModule({providers: [AgentService,{ provide: ToolRegistryService, useValue: {getTool: () => mock}}]}).compile();// ...});
五、性能优化实践
5.1 输出缓存策略
对稳定输出实施缓存:
// src/agent/services/output-cache.service.ts@Injectable()export class OutputCacheService {constructor(@InjectRedis() private readonly redis: Redis,private readonly agentService: AgentService) {}async getCachedOutput(inputHash: string): Promise<any> {const cached = await this.redis.get(`output:${inputHash}`);return cached ? JSON.parse(cached) : null;}async processWithCache(input: AgentInputDto): Promise<any> {const inputHash = createHash('md5').update(JSON.stringify(input)).digest('hex');const cached = await this.getCachedOutput(inputHash);if (cached) return cached;const result = await this.agentService.process(input);await this.redis.setex(`output:${inputHash}`, 3600, JSON.stringify(result));return result;}}
缓存策略选择:
- 参数化查询:对相同输入返回相同输出的情况适用
- 版本控制:当工具集变更时清空相关缓存
- 梯度失效:重要数据设置较短TTL,静态数据设置长TTL
5.2 工具池复用
对耗时工具实现连接池:
// src/tools/pool/tool-pool.service.ts@Injectable()export class ToolPoolService {private pool = new Map<string, Pool<ToolInterface>>();acquireTool(name: string): Promise<ToolInterface> {if (!this.pool.has(name)) {const factory = () => this.createToolInstance(name);this.pool.set(name, new Pool({ factory, max: 5 }));}return this.pool.get(name)!.acquire();}releaseTool(name: string, tool: ToolInterface): Promise<void> {const pool = this.pool.get(name);if (pool) return pool.release(tool);return Promise.resolve();}}
六、安全与合规考量
6.1 输出脱敏处理
实现敏感数据过滤中间件:
// src/common/middleware/output-sanitizer.middleware.tsexport class OutputSanitizerMiddleware implements NestMiddleware {private readonly sensitivePatterns = [/(\d{3})-\d{2}-\d{4}/g, // SSN/(\d{3})[\s-]?\d{3}[\s-]?\d{4}/g // 信用卡];use(req: Request, res: Response, next: NextFunction) {const originalJson = res.json;res.json = (body: any) => {const sanitized = this.sanitize(body);return originalJson.call(res, sanitized);};next();}private sanitize(data: any): any {if (typeof data !== 'object' || data === null) return data;return Object.fromEntries(Object.entries(data).map(([key, value]) => {if (typeof value === 'string') {return [key, this.applyPatterns(value)];} else if (typeof value === 'object') {return [key, this.sanitize(value)];}return [key, value];}));}}
6.2 工具调用鉴权
基于角色的工具访问控制:
// src/tools/auth/tool-auth.guard.ts@Injectable()export class ToolAuthGuard implements CanActivate {constructor(private readonly reflector: Reflector,private readonly toolAuthService: ToolAuthService) {}async canActivate(context: ExecutionContext): Promise<boolean> {const requiredTools = this.reflector.get<string[]>('requiredTools',context.getHandler());if (!requiredTools) return true;const request = context.switchToHttp().getRequest();const user = request.user;return this.toolAuthService.checkAccess(user.id,requiredTools);}}// 在控制器方法上使用装饰器export const RequiredTools = (tools: string[]) =>SetMetadata('requiredTools', tools);@UseGuards(ToolAuthGuard)@RequiredTools(['database', 'file-system'])@Post('/sensitive-operation')async sensitiveOperation() {// ...}
通过系统化的结构化输出设计和工具调用管理,NestJS智能体可实现高可维护性、强安全性的业务逻辑处理。开发者应重点关注DTO设计的合理性、异步调用的错误处理以及性能瓶颈的提前识别,结合具体业务场景选择合适的缓存策略和安全控制方案。