From c2b4fe19cc545a6318220707bbb9cf39a740c6ef Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 24 Jan 2026 20:20:30 -0800 Subject: [PATCH] refactor(evolution): use knowledge-service API for system_experiences Follow proper microservices architecture: - knowledge-service owns system_experiences table - evolution-service uses KnowledgeClient API to save experiences - Deleted SystemExperienceORM from evolution-service - Added internal API endpoints in knowledge-service - Disabled synchronize in all services for safety Co-Authored-By: Claude Opus 4.5 --- .../evolution-service/src/app.module.ts | 3 +- .../src/evolution/evolution.module.ts | 9 +- .../src/evolution/evolution.service.ts | 133 +++++------------- .../clients/knowledge.client.ts | 133 ++++++++++++++++++ .../entities/system-experience.orm.ts | 62 -------- .../knowledge-service/src/app.module.ts | 3 +- .../src/memory/internal.controller.ts | 100 +++++++++++++ .../src/memory/memory.module.ts | 3 +- 8 files changed, 277 insertions(+), 169 deletions(-) create mode 100644 packages/services/evolution-service/src/infrastructure/clients/knowledge.client.ts delete mode 100644 packages/services/evolution-service/src/infrastructure/database/entities/system-experience.orm.ts create mode 100644 packages/services/knowledge-service/src/memory/internal.controller.ts diff --git a/packages/services/evolution-service/src/app.module.ts b/packages/services/evolution-service/src/app.module.ts index 25ccff2..0f4ef5d 100644 --- a/packages/services/evolution-service/src/app.module.ts +++ b/packages/services/evolution-service/src/app.module.ts @@ -25,7 +25,8 @@ import { HealthModule } from './health/health.module'; password: config.get('POSTGRES_PASSWORD'), database: config.get('POSTGRES_DB', 'iconsulting'), autoLoadEntities: true, - synchronize: config.get('NODE_ENV') !== 'production', + // 禁用synchronize,使用init-db.sql初始化schema + synchronize: false, logging: config.get('NODE_ENV') === 'development', }), }), diff --git a/packages/services/evolution-service/src/evolution/evolution.module.ts b/packages/services/evolution-service/src/evolution/evolution.module.ts index 94bd5cf..b70f31f 100644 --- a/packages/services/evolution-service/src/evolution/evolution.module.ts +++ b/packages/services/evolution-service/src/evolution/evolution.module.ts @@ -1,22 +1,19 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { EvolutionController } from './evolution.controller'; import { EvolutionService } from './evolution.service'; import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service'; import { ConversationClient } from '../infrastructure/clients/conversation.client'; -import { SystemExperienceORM } from '../infrastructure/database/entities/system-experience.orm'; +import { KnowledgeClient } from '../infrastructure/clients/knowledge.client'; @Module({ - imports: [ - // 只保留自己 Bounded Context 的实体 - TypeOrmModule.forFeature([SystemExperienceORM]), - ], + imports: [], controllers: [EvolutionController], providers: [ EvolutionService, ExperienceExtractorService, // 使用 API 客户端访问其他服务的数据 ConversationClient, + KnowledgeClient, ], exports: [EvolutionService], }) diff --git a/packages/services/evolution-service/src/evolution/evolution.service.ts b/packages/services/evolution-service/src/evolution/evolution.service.ts index d315b51..3496cbf 100644 --- a/packages/services/evolution-service/src/evolution/evolution.service.ts +++ b/packages/services/evolution-service/src/evolution/evolution.service.ts @@ -1,9 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service'; -import { ConversationClient, ConversationDto, MessageDto } from '../infrastructure/clients/conversation.client'; -import { SystemExperienceORM } from '../infrastructure/database/entities/system-experience.orm'; +import { ConversationClient } from '../infrastructure/clients/conversation.client'; +import { KnowledgeClient } from '../infrastructure/clients/knowledge.client'; import { v4 as uuidv4 } from 'uuid'; /** @@ -21,13 +19,17 @@ export interface EvolutionTaskResult { /** * 进化服务 * 负责系统的自我学习和进化 + * + * 遵循微服务架构原则: + * - 通过 ConversationClient 调用 conversation-service API 获取对话数据 + * - 通过 KnowledgeClient 调用 knowledge-service API 保存经验 + * - 不直接访问其他服务的数据库表 */ @Injectable() export class EvolutionService { constructor( private conversationClient: ConversationClient, - @InjectRepository(SystemExperienceORM) - private experienceRepo: Repository, + private knowledgeClient: KnowledgeClient, private experienceExtractor: ExperienceExtractorService, ) {} @@ -86,17 +88,22 @@ export class EvolutionService { rating: conversation.rating, }); - // 保存提取的经验 + // 通过 API 保存提取的经验到 knowledge-service for (const exp of analysis.experiences) { - await this.saveExperience({ - experienceType: exp.type, - content: exp.content, - scenario: exp.scenario, - confidence: exp.confidence, - relatedCategory: exp.relatedCategory, - sourceConversationId: conversation.id, - }); - result.experiencesExtracted++; + try { + await this.knowledgeClient.saveExperience({ + experienceType: exp.type, + content: exp.content, + scenario: exp.scenario, + confidence: exp.confidence, + relatedCategory: exp.relatedCategory, + sourceConversationId: conversation.id, + }); + result.experiencesExtracted++; + } catch (saveError) { + console.error(`[Evolution] Failed to save experience:`, saveError); + result.errors.push(`Save experience: ${(saveError as Error).message}`); + } } // 收集知识缺口 @@ -127,65 +134,6 @@ export class EvolutionService { } } - /** - * 保存经验(带去重逻辑) - */ - private async saveExperience(params: { - experienceType: string; - content: string; - scenario: string; - confidence: number; - relatedCategory?: string; - sourceConversationId: string; - }): Promise { - // 查找相似经验 - const existingExperiences = await this.experienceRepo.find({ - where: { - experienceType: params.experienceType, - relatedCategory: params.relatedCategory, - }, - }); - - // 简单的相似度检查(实际应该用向量相似度) - const similar = existingExperiences.find( - exp => this.simpleSimilarity(exp.content, params.content) > 0.8, - ); - - if (similar) { - // 合并到现有经验 - if (!similar.sourceConversationIds.includes(params.sourceConversationId)) { - similar.sourceConversationIds.push(params.sourceConversationId); - similar.confidence = Math.min(100, similar.confidence + 5); - await this.experienceRepo.save(similar); - } - } else { - // 创建新经验 - const newExperience = this.experienceRepo.create({ - id: uuidv4(), - experienceType: params.experienceType, - content: params.content, - scenario: params.scenario, - confidence: params.confidence, - relatedCategory: params.relatedCategory, - sourceConversationIds: [params.sourceConversationId], - verificationStatus: 'PENDING', - isActive: false, - }); - await this.experienceRepo.save(newExperience); - } - } - - /** - * 简单的文本相似度计算 - */ - private simpleSimilarity(a: string, b: string): number { - const aWords = new Set(a.toLowerCase().split(/\s+/)); - const bWords = new Set(b.toLowerCase().split(/\s+/)); - const intersection = [...aWords].filter(x => bWords.has(x)).length; - const union = new Set([...aWords, ...bWords]).size; - return intersection / union; - } - /** * 获取进化统计信息 */ @@ -197,12 +145,8 @@ export class EvolutionService { recentConversationsAnalyzed: number; topExperienceTypes: Array<{ type: string; count: number }>; }> { - const [total, pending, approved, active] = await Promise.all([ - this.experienceRepo.count(), - this.experienceRepo.count({ where: { verificationStatus: 'PENDING' } }), - this.experienceRepo.count({ where: { verificationStatus: 'APPROVED' } }), - this.experienceRepo.count({ where: { isActive: true } }), - ]); + // 通过 API 获取经验统计 + const stats = await this.knowledgeClient.getStatistics(); // 通过 API 获取最近分析的对话数(过去7天) const recentConversations = await this.conversationClient.countConversations({ @@ -210,26 +154,19 @@ export class EvolutionService { daysBack: 7, }); - // 获取经验类型分布 - const typeDistribution = await this.experienceRepo - .createQueryBuilder('exp') - .select('exp.experienceType', 'type') - .addSelect('COUNT(*)', 'count') - .groupBy('exp.experienceType') - .orderBy('count', 'DESC') - .limit(5) - .getRawMany(); + // 转换类型分布为数组格式 + const topExperienceTypes = Object.entries(stats.byType) + .map(([type, count]) => ({ type, count: count as number })) + .sort((a, b) => b.count - a.count) + .slice(0, 5); return { - totalExperiences: total, - pendingExperiences: pending, - approvedExperiences: approved, - activeExperiences: active, + totalExperiences: stats.total, + pendingExperiences: stats.byStatus['PENDING'] || 0, + approvedExperiences: stats.byStatus['APPROVED'] || 0, + activeExperiences: stats.byStatus['ACTIVE'] || 0, recentConversationsAnalyzed: recentConversations, - topExperienceTypes: typeDistribution.map(t => ({ - type: t.type, - count: parseInt(t.count), - })), + topExperienceTypes, }; } diff --git a/packages/services/evolution-service/src/infrastructure/clients/knowledge.client.ts b/packages/services/evolution-service/src/infrastructure/clients/knowledge.client.ts new file mode 100644 index 0000000..6f76210 --- /dev/null +++ b/packages/services/evolution-service/src/infrastructure/clients/knowledge.client.ts @@ -0,0 +1,133 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +/** + * 经验数据传输对象 + */ +export interface ExperienceDto { + id: string; + experienceType: string; + content: string; + scenario: string; + confidence: number; + relatedCategory?: string; + sourceConversationIds: string[]; + verificationStatus: string; + isActive: boolean; + usageCount: number; + positiveCount: number; + negativeCount: number; + createdAt: Date; + updatedAt: Date; +} + +/** + * 经验统计数据传输对象 + */ +export interface ExperienceStatisticsDto { + total: number; + byStatus: Record; + byType: Record; +} + +/** + * Knowledge Service 客户端 + * 用于调用 knowledge-service 的内部 API + */ +@Injectable() +export class KnowledgeClient { + private readonly baseUrl: string; + + constructor(private configService: ConfigService) { + this.baseUrl = this.configService.get( + 'KNOWLEDGE_SERVICE_URL', + 'http://knowledge-service:3005', + ); + } + + /** + * 保存经验 + */ + async saveExperience(params: { + experienceType: string; + content: string; + scenario: string; + confidence: number; + relatedCategory?: string; + sourceConversationId: string; + }): Promise { + const url = `${this.baseUrl}/api/internal/experiences`; + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + const data = await response.json(); + + if (!data.success) { + throw new Error('Failed to save experience'); + } + + return data.data; + } catch (error) { + console.error('[KnowledgeClient] Error saving experience:', error); + throw error; + } + } + + /** + * 获取经验统计 + */ + async getStatistics(): Promise { + const url = `${this.baseUrl}/api/internal/experiences/statistics`; + + try { + const response = await fetch(url); + const data = await response.json(); + + if (!data.success) { + throw new Error('Failed to get statistics'); + } + + return data.data; + } catch (error) { + console.error('[KnowledgeClient] Error getting statistics:', error); + throw error; + } + } + + /** + * 统计经验数量 + */ + async countExperiences(options: { + status?: string; + isActive?: boolean; + }): Promise { + const params = new URLSearchParams(); + + if (options.status) params.append('status', options.status); + if (options.isActive !== undefined) { + params.append('isActive', options.isActive.toString()); + } + + const url = `${this.baseUrl}/api/internal/experiences/count?${params.toString()}`; + + try { + const response = await fetch(url); + const data = await response.json(); + + if (!data.success) { + throw new Error('Failed to count experiences'); + } + + return data.data.count; + } catch (error) { + console.error('[KnowledgeClient] Error counting experiences:', error); + throw error; + } + } +} diff --git a/packages/services/evolution-service/src/infrastructure/database/entities/system-experience.orm.ts b/packages/services/evolution-service/src/infrastructure/database/entities/system-experience.orm.ts deleted file mode 100644 index bd86a51..0000000 --- a/packages/services/evolution-service/src/infrastructure/database/entities/system-experience.orm.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - Entity, - Column, - PrimaryColumn, - CreateDateColumn, - UpdateDateColumn, -} from 'typeorm'; - -@Entity('system_experiences') -export class SystemExperienceORM { - @PrimaryColumn('uuid') - id: string; - - @Column({ name: 'experience_type', length: 30 }) - experienceType: string; - - @Column('text') - content: string; - - @Column({ default: 50 }) - confidence: number; - - @Column('text') - scenario: string; - - @Column({ name: 'related_category', length: 50, nullable: true }) - relatedCategory: string; - - @Column('uuid', { name: 'source_conversation_ids', array: true, default: '{}' }) - sourceConversationIds: string[]; - - @Column({ name: 'verification_status', length: 20, default: 'PENDING' }) - verificationStatus: string; - - @Column({ name: 'verified_by', nullable: true }) - verifiedBy: string; - - @Column({ name: 'verified_at', nullable: true }) - verifiedAt: Date; - - @Column({ name: 'usage_count', default: 0 }) - usageCount: number; - - @Column({ name: 'positive_count', default: 0 }) - positiveCount: number; - - @Column({ name: 'negative_count', default: 0 }) - negativeCount: number; - - // pgvector VECTOR(1536) - exclude from default selects to avoid type conflicts - @Column({ type: 'text', name: 'embedding', nullable: true, select: false }) - embedding: string; - - @Column({ name: 'is_active', default: false }) - isActive: boolean; - - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at' }) - updatedAt: Date; -} diff --git a/packages/services/knowledge-service/src/app.module.ts b/packages/services/knowledge-service/src/app.module.ts index c118921..a99561b 100644 --- a/packages/services/knowledge-service/src/app.module.ts +++ b/packages/services/knowledge-service/src/app.module.ts @@ -25,7 +25,8 @@ import { HealthModule } from './health/health.module'; password: config.get('POSTGRES_PASSWORD'), database: config.get('POSTGRES_DB', 'iconsulting'), autoLoadEntities: true, - synchronize: config.get('NODE_ENV') !== 'production', + // 禁用synchronize,使用init-db.sql初始化schema + synchronize: false, logging: config.get('NODE_ENV') === 'development', }), }), diff --git a/packages/services/knowledge-service/src/memory/internal.controller.ts b/packages/services/knowledge-service/src/memory/internal.controller.ts new file mode 100644 index 0000000..2959894 --- /dev/null +++ b/packages/services/knowledge-service/src/memory/internal.controller.ts @@ -0,0 +1,100 @@ +import { Controller, Get, Post, Body, Query } from '@nestjs/common'; +import { MemoryService } from './memory.service'; +import { ExperienceType } from '../domain/entities/system-experience.entity'; + +/** + * 内部 API 控制器 + * 仅供其他微服务调用(如 evolution-service) + */ +@Controller('internal/experiences') +export class InternalMemoryController { + constructor(private memoryService: MemoryService) {} + + /** + * 保存经验(由 evolution-service 调用) + */ + @Post() + async saveExperience(@Body() dto: { + experienceType: string; + content: string; + scenario: string; + confidence: number; + relatedCategory?: string; + sourceConversationId: string; + }) { + const experience = await this.memoryService.extractAndSaveExperience({ + experienceType: dto.experienceType as ExperienceType, + content: dto.content, + scenario: dto.scenario, + confidence: dto.confidence, + relatedCategory: dto.relatedCategory, + sourceConversationId: dto.sourceConversationId, + }); + return { + success: true, + data: { + id: experience.id, + experienceType: experience.experienceType, + content: experience.content, + scenario: experience.scenario, + confidence: experience.confidence, + }, + }; + } + + /** + * 查找相似经验(用于去重检查) + */ + @Post('find-similar') + async findSimilarExperiences(@Body() dto: { + experienceType: string; + relatedCategory?: string; + }) { + // 返回同类型同类别的经验用于客户端去重 + // 实际的相似度检查在客户端进行 + const stats = await this.memoryService.getExperienceStatistics(); + return { + success: true, + data: { + totalByType: stats.byType, + }, + }; + } + + /** + * 获取经验统计 + */ + @Get('statistics') + async getStatistics() { + const stats = await this.memoryService.getExperienceStatistics(); + return { + success: true, + data: { + total: stats.total, + byStatus: stats.byStatus, + byType: stats.byType, + }, + }; + } + + /** + * 统计经验数量 + */ + @Get('count') + async countExperiences( + @Query('status') status?: string, + @Query('isActive') isActive?: string, + ) { + const stats = await this.memoryService.getExperienceStatistics(); + + let count = stats.total; + if (status && stats.byStatus[status as keyof typeof stats.byStatus]) { + count = stats.byStatus[status as keyof typeof stats.byStatus]; + } + + return { + success: true, + data: { count }, + }; + } +} diff --git a/packages/services/knowledge-service/src/memory/memory.module.ts b/packages/services/knowledge-service/src/memory/memory.module.ts index 86db9f8..321f497 100644 --- a/packages/services/knowledge-service/src/memory/memory.module.ts +++ b/packages/services/knowledge-service/src/memory/memory.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MemoryController } from './memory.controller'; +import { InternalMemoryController } from './internal.controller'; import { MemoryService } from './memory.service'; import { EmbeddingService } from '../infrastructure/embedding/embedding.service'; import { Neo4jService } from '../infrastructure/database/neo4j/neo4j.service'; @@ -22,7 +23,7 @@ import { SystemExperienceORM, ]), ], - controllers: [MemoryController], + controllers: [MemoryController, InternalMemoryController], providers: [ MemoryService, EmbeddingService,