<p align="right"><font color="#3f3f3f">2025年06月13日</font></p>
## 1. 项目概述
### 1.1 项目目标
构建一个类似 Claude.ai 的 AI 对话应用,支持多模型对话、代码执行、数据分析等功能。项目设计遵循轻量级原则,可在单机 MacBook 上运行,同时通过良好的抽象层设计确保未来的可扩展性。
### 1.2 核心功能
- **AI 对话**:支持 Claude、DeepSeek、Qwen 等模型
- **流式响应**:实时展示 AI 回复
- **Artifacts**:代码展示和前端代码执行
- **Analysis Tool**:数据分析和可视化
- **代码执行**:安全的多语言代码执行环境
- **LLM 监控**:全链路追踪、质量评估、业务指标监控
### 1.3 技术原则
- **轻量级优先**:最小化外部依赖,确保本地可运行
- **抽象层设计**:便于后续替换和扩展各个组件
- **渐进式增强**:从 MVP 开始,逐步添加高级功能
## 2. 技术栈选型
### 2.1 后端技术栈
- **核心框架**:Spring Boot 3.x + Java 17+
- **AI 框架**:LangChain4j
- **实时通信**:Spring WebSocket + STOMP
- **数据存储**:H2 Database(嵌入式)
- **缓存方案**:Caffeine(JVM 内存缓存)
- **代码执行**:Docker API(轻量级容器)
- **LLM 监控**:Langfuse + OpenTelemetry
- **指标收集**:Micrometer + Prometheus
- **日志聚合**:Logback + 结构化日志
### 2.2 前端技术栈
- **框架**:React 18
- **UI 组件**:Ant Design 5.x
- **代码编辑器**:Monaco Editor
- **Markdown 渲染**:react-markdown
- **WebSocket 客户端**:socket.io-client
- **构建工具**:Vite
## 3. 系统架构设计
### 3.1 整体架构图
```
┌─────────────────────────────────────────────────────┐
│ React 前端应用 │
│ ┌─────────┐ ┌─────────────┐ ┌────────────────┐ │
│ │会话列表 │ │ 聊天界面 │ │ Artifacts面板 │ │
│ └─────────┘ └─────────────┘ └────────────────┘ │
└────────────────────────┬────────────────────────────┘
│ WebSocket + REST API
┌────────────────────────┴────────────────────────────┐
│ Spring Boot 应用 │
├─────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ Controller │ │ Service层 │ │抽象层接口 │ │
│ │ - Chat │ │ - Chat │ │ - LLM │ │
│ │ - Code │ │ - Execution │ │ - Storage │ │
│ │ - Analysis │ │ - Analysis │ │ - Executor│ │
│ │ - Monitor │ │ - Monitor │ │ - Monitor │ │
│ └─────────────┘ └──────────────┘ └───────────┘ │
├─────────────────────────────────────────────────────┤
│ 具体实现层 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────┐ │
│ │Claude │ │Docker │ │ H2 │ │Local │ │
│ │DeepSeek│ │Executor│ │Database│ │Python │ │
│ │Qwen │ │ │ │ │ │ │ │
│ └────────┘ └────────┘ └────────┘ └──────────┘ │
├─────────────────────────────────────────────────────┤
│ 监控和可观测性 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────┐ │
│ │Langfuse│ │OpenTel │ │Metrics │ │Dashboard │ │
│ │Tracing │ │Tracing │ │Export │ │Grafana │ │
│ └────────┘ └────────┘ └────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘
```
### 3.2 核心组件交互流程
```mermaid
sequenceDiagram
participant U as 用户
participant F as 前端
participant W as WebSocket
participant C as ChatService
participant L as LLMProvider
participant E as CodeExecutor
participant S as Storage
U->>F: 发送消息
F->>W: WebSocket消息
W->>C: 处理消息
C->>S: 保存消息
C->>L: 调用AI模型
L-->>C: 流式响应
C-->>W: 推送响应块
W-->>F: 显示流式文本
alt 包含代码执行
C->>E: 执行代码
E-->>C: 执行结果
C-->>W: 推送结果
W-->>F: 显示执行结果
end
C->>S: 保存完整对话
```
## 4. 抽象层设计
### 4.1 LLM Provider 抽象
```java
// LLMProvider.java
public interface LLMProvider {
String getName();
boolean isAvailable();
ChatResponse chat(ChatRequest request);
CompletableFuture<ChatResponse> chatAsync(ChatRequest request);
Stream<ChatChunk> streamChat(ChatRequest request);
}
// ChatRequest.java
@Data
@Builder
public class ChatRequest {
private String model;
private List<Message> messages;
private Double temperature;
private Integer maxTokens;
private boolean stream;
private Map<String, Object> additionalParams;
}
// LLMProviderFactory.java
@Component
public class LLMProviderFactory {
private final Map<String, LLMProvider> providers;
private final String defaultProvider;
@Autowired
public LLMProviderFactory(
List<LLMProvider> providerList,
@Value("${llm.default-provider}") String defaultProvider
) {
this.providers = providerList.stream()
.collect(Collectors.toMap(LLMProvider::getName, Function.identity()));
this.defaultProvider = defaultProvider;
}
public LLMProvider getProvider(String name) {
return providers.getOrDefault(name, providers.get(defaultProvider));
}
}
```
### 4.2 Code Executor 抽象
```java
// CodeExecutor.java
public interface CodeExecutor {
ExecutionResult execute(ExecutionRequest request);
boolean supports(String language);
ExecutionEnvironment getEnvironment();
int getPriority(); // 用于选择最合适的执行器
}
// ExecutionRequest.java
@Data
@Builder
public class ExecutionRequest {
private String language;
private String code;
private Map<String, String> files;
private ExecutionConstraints constraints;
private Map<String, Object> metadata;
}
// ExecutionConstraints.java
@Data
@Builder
public class ExecutionConstraints {
@Builder.Default
private Duration timeout = Duration.ofSeconds(30);
@Builder.Default
private Long memoryLimitMB = 512L;
@Builder.Default
private Double cpuLimit = 0.5;
@Builder.Default
private boolean allowNetwork = false;
private List<String> allowedPackages;
}
// CodeExecutorRegistry.java
@Component
public class CodeExecutorRegistry {
private final List<CodeExecutor> executors;
@Autowired
public CodeExecutorRegistry(List<CodeExecutor> executors) {
this.executors = executors.stream()
.sorted(Comparator.comparing(CodeExecutor::getPriority).reversed())
.collect(Collectors.toList());
}
public CodeExecutor getExecutor(String language) {
return executors.stream()
.filter(e -> e.supports(language))
.findFirst()
.orElseThrow(() -> new UnsupportedLanguageException(language));
}
}
```
### 4.3 Storage 抽象
```java
// StorageService.java
public interface StorageService {
// 对话存储
Conversation saveConversation(Conversation conversation);
Conversation getConversation(String id);
Page<Conversation> listConversations(String userId, Pageable pageable);
void deleteConversation(String id);
// 消息存储
Message saveMessage(Message message);
List<Message> getMessages(String conversationId);
// 文件存储
FileInfo saveFile(MultipartFile file, FileMetadata metadata);
Resource loadFile(String fileId);
void deleteFile(String fileId);
// 缓存操作
void cache(String key, Object value, Duration ttl);
<T> Optional<T> getFromCache(String key, Class<T> type);
void evictCache(String key);
}
// 组合实现
@Component
public class CompositeStorageService implements StorageService {
private final ConversationRepository conversationRepo;
private final FileStorage fileStorage;
private final CacheManager cacheManager;
// 实现方法委托给具体组件
}
```
### 4.4 Analysis Engine 抽象
```java
// AnalysisEngine.java
public interface AnalysisEngine {
String getType();
boolean supports(String language);
AnalysisResult analyze(AnalysisRequest request);
CompletableFuture<AnalysisResult> analyzeAsync(AnalysisRequest request);
void cancelAnalysis(String analysisId);
}
// AnalysisRequest.java
@Data
@Builder
public class AnalysisRequest {
private String id;
private String language;
private String code;
private Map<String, Object> inputData;
private AnalysisOptions options;
}
// AnalysisOptions.java
@Data
@Builder
public class AnalysisOptions {
@Builder.Default
private boolean generateVisualization = true;
@Builder.Default
private String outputFormat = "html";
private List<String> requiredLibraries;
private Map<String, String> environment;
}
```
### 4.5 LLM 监控和可观测性抽象
```java
// LLMObservabilityService.java
public interface LLMObservabilityService {
// 追踪管理
TraceContext startTrace(String operationType, Map<String, Object> metadata);
void endTrace(TraceContext context, TraceResult result);
void addTraceEvent(TraceContext context, String event, Map<String, Object> data);
// 指标收集
void recordLatency(String provider, String model, Duration duration);
void recordTokenUsage(String provider, String model, TokenUsage usage);
void recordError(String provider, String model, String errorType, Throwable error);
void recordUserFeedback(String conversationId, String messageId, UserFeedback feedback);
// 质量评估
void evaluateResponse(String conversationId, String messageId, ResponseEvaluation evaluation);
CompletableFuture<QualityMetrics> calculateQualityMetrics(String timeRange);
// 业务指标
void recordBusinessEvent(String eventType, Map<String, Object> properties);
void recordConversationMetrics(ConversationMetrics metrics);
}
// TraceContext.java
@Data
@Builder
public class TraceContext {
private String traceId;
private String spanId;
private String parentSpanId;
private String operationType;
private Instant startTime;
private Map<String, Object> metadata;
private Map<String, String> tags;
}
// TokenUsage.java
@Data
@Builder
public class TokenUsage {
private int inputTokens;
private int outputTokens;
private int totalTokens;
private BigDecimal cost;
private String costCurrency;
}
// ResponseEvaluation.java
@Data
@Builder
public class ResponseEvaluation {
private String evaluationId;
private String messageId;
private String conversationId;
private QualityScore qualityScore;
private List<String> issues;
private Map<String, Object> metadata;
private Instant evaluatedAt;
}
// QualityScore.java
@Data
@Builder
public class QualityScore {
@Builder.Default
private Double relevance = 0.0;
@Builder.Default
private Double accuracy = 0.0;
@Builder.Default
private Double helpfulness = 0.0;
@Builder.Default
private Double safety = 0.0;
@Builder.Default
private Double overall = 0.0;
private String evaluationMethod;
}
// ConversationMetrics.java
@Data
@Builder
public class ConversationMetrics {
private String conversationId;
private String userId;
private int messageCount;
private Duration totalDuration;
private Duration averageResponseTime;
private int errorCount;
private UserSatisfaction satisfaction;
private List<String> topics;
private Map<String, Object> businessMetrics;
}
// LLMMonitoringConfig.java
@Data
@Builder
public class LLMMonitoringConfig {
@Builder.Default
private boolean enableTracing = true;
@Builder.Default
private boolean enableMetrics = true;
@Builder.Default
private boolean enableQualityEvaluation = true;
@Builder.Default
private boolean enableUserFeedback = true;
@Builder.Default
private double samplingRate = 1.0;
private Set<String> enabledProviders;
private Map<String, Object> customConfig;
}
```
## 5. 具体实现方案
### 5.1 项目结构
```
claude-clone/
├── backend/
│ ├── src/main/java/com/claude/
│ │ ├── controller/
│ │ │ ├── ChatController.java
│ │ │ ├── CodeExecutionController.java
│ │ │ └── AnalysisController.java
│ │ ├── service/
│ │ │ ├── ChatService.java
│ │ │ ├── ExecutionService.java
│ │ │ └── AnalysisService.java
│ │ ├── provider/
│ │ │ ├── llm/
│ │ │ │ ├── ClaudeLLMProvider.java
│ │ │ │ ├── DeepSeekLLMProvider.java
│ │ │ │ └── QwenLLMProvider.java
│ │ │ ├── executor/
│ │ │ │ ├── FrontendCodeExecutor.java
│ │ │ │ └── DockerCodeExecutor.java
│ │ │ └── analysis/
│ │ │ └── LocalPythonAnalysisEngine.java
│ │ ├── storage/
│ │ │ ├── entity/
│ │ │ ├── repository/
│ │ │ └── service/
│ │ ├── config/
│ │ │ ├── WebSocketConfig.java
│ │ │ ├── SecurityConfig.java
│ │ │ └── CacheConfig.java
│ │ └── util/
│ ├── src/main/resources/
│ │ ├── application.yml
│ │ └── application-prod.yml
│ └── pom.xml
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ │ ├── Chat/
│ │ │ ├── Artifacts/
│ │ │ └── Analysis/
│ │ ├── services/
│ │ ├── hooks/
│ │ ├── utils/
│ │ └── App.tsx
│ ├── package.json
│ └── vite.config.ts
├── docker/
│ ├── Dockerfile
│ └── docker-compose.yml
├── scripts/
│ ├── start.sh
│ └── setup.sh
└── README.md
```
### 5.2 后端核心实现
#### 5.2.1 WebSocket 配置
```java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
```
#### 5.2.2 Chat Controller
```java
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatService chatService;
private final SimpMessagingTemplate messagingTemplate;
@MessageMapping("/chat.send")
public void handleChatMessage(
@Payload ChatMessage message,
@Header("simpSessionId") String sessionId
) {
// 保存用户消息
chatService.saveUserMessage(message);
// 流式处理AI响应
chatService.streamChat(message)
.doOnNext(chunk -> {
// 推送每个响应块到前端
messagingTemplate.convertAndSendToUser(
sessionId,
"/queue/messages",
chunk
);
})
.doOnComplete(() -> {
// 发送完成信号
messagingTemplate.convertAndSendToUser(
sessionId,
"/queue/messages",
ChatChunk.completed()
);
})
.subscribe();
}
}
```
#### 5.2.3 Claude Provider 实现
```java
@Component
@ConditionalOnProperty(name = "llm.providers.claude.enabled", havingValue = "true")
public class ClaudeLLMProvider implements LLMProvider {
private final AnthropicChatModel model;
public ClaudeLLMProvider(@Value("${llm.providers.claude.api-key}") String apiKey) {
this.model = AnthropicChatModel.builder()
.apiKey(apiKey)
.modelName("claude-3-sonnet-20240229")
.build();
}
@Override
public String getName() {
return "claude";
}
@Override
public Stream<ChatChunk> streamChat(ChatRequest request) {
return model.stream(toLangChainRequest(request))
.map(this::toChatChunk);
}
}
```
#### 5.2.4 Docker 执行器实现
```java
@Component
@ConditionalOnProperty(name = "execution.docker.enabled", havingValue = "true")
public class DockerCodeExecutor implements CodeExecutor {
private static final Map<String, String> LANGUAGE_IMAGES = Map.of(
"python", "python:3.11-alpine",
"javascript", "node:20-alpine",
"java", "openjdk:17-alpine"
);
@Override
public ExecutionResult execute(ExecutionRequest request) {
String image = LANGUAGE_IMAGES.get(request.getLanguage());
String containerId = createContainer(image, request);
try {
// 写入代码文件
writeCodeToContainer(containerId, request.getCode());
// 执行代码
ExecCreateResponse execResponse = dockerClient.execCreate(
containerId,
new String[]{getExecuteCommand(request.getLanguage())},
DockerClient.ExecCreateParam.attachStdout(),
DockerClient.ExecCreateParam.attachStderr()
);
// 获取输出
LogStream logStream = dockerClient.execStart(execResponse.id());
String output = logStream.readFully();
return ExecutionResult.success(output);
} finally {
// 清理容器
dockerClient.removeContainer(containerId);
}
}
private String createContainer(String image, ExecutionRequest request) {
ContainerConfig config = ContainerConfig.builder()
.image(image)
.hostConfig(HostConfig.builder()
.memory(request.getConstraints().getMemoryLimitMB() * 1024 * 1024)
.cpuQuota((long)(request.getConstraints().getCpuLimit() * 100000))
.networkMode("none")
.readonlyRootfs(true)
.build())
.build();
return dockerClient.createContainer(config).id();
}
}
```
#### 5.2.5 LLM 监控实现
```java
// LangfuseObservabilityService.java
@Component
@ConditionalOnProperty(name = "monitoring.langfuse.enabled", havingValue = "true")
public class LangfuseObservabilityService implements LLMObservabilityService {
private final LangfuseClient langfuseClient;
private final MeterRegistry meterRegistry;
private final ObjectMapper objectMapper;
@Autowired
public LangfuseObservabilityService(
@Value("${monitoring.langfuse.api-key}") String apiKey,
@Value("${monitoring.langfuse.host}") String host,
MeterRegistry meterRegistry,
ObjectMapper objectMapper
) {
this.langfuseClient = LangfuseClient.builder()
.apiKey(apiKey)
.host(host)
.build();
this.meterRegistry = meterRegistry;
this.objectMapper = objectMapper;
}
@Override
public TraceContext startTrace(String operationType, Map<String, Object> metadata) {
String traceId = UUID.randomUUID().toString();
String spanId = UUID.randomUUID().toString();
// Langfuse 追踪
langfuseClient.trace(CreateTraceRequest.builder()
.id(traceId)
.name(operationType)
.metadata(metadata)
.timestamp(Instant.now())
.build());
// OpenTelemetry 追踪
Span span = tracer.spanBuilder(operationType)
.setSpanKind(SpanKind.INTERNAL)
.startSpan();
span.setAllAttributes(Attributes.of(
AttributeKey.stringKey("trace.id"), traceId,
AttributeKey.stringKey("operation.type"), operationType
));
return TraceContext.builder()
.traceId(traceId)
.spanId(spanId)
.operationType(operationType)
.startTime(Instant.now())
.metadata(metadata)
.build();
}
@Override
public void recordLatency(String provider, String model, Duration duration) {
// Micrometer 指标
Timer.Sample sample = Timer.start(meterRegistry);
sample.stop(Timer.builder("llm.request.duration")
.tag("provider", provider)
.tag("model", model)
.register(meterRegistry));
// Langfuse 指标
langfuseClient.score(CreateScoreRequest.builder()
.name("latency")
.value(duration.toMillis())
.build());
}
@Override
public void recordTokenUsage(String provider, String model, TokenUsage usage) {
// 记录 Token 使用量
meterRegistry.counter("llm.tokens.input",
"provider", provider,
"model", model).increment(usage.getInputTokens());
meterRegistry.counter("llm.tokens.output",
"provider", provider,
"model", model).increment(usage.getOutputTokens());
if (usage.getCost() != null) {
meterRegistry.counter("llm.cost",
"provider", provider,
"model", model,
"currency", usage.getCostCurrency()).increment(usage.getCost().doubleValue());
}
}
@Override
public void evaluateResponse(String conversationId, String messageId, ResponseEvaluation evaluation) {
// 异步质量评估
CompletableFuture.runAsync(() -> {
try {
// 调用质量评估模型
QualityScore score = performQualityEvaluation(evaluation);
// 存储评估结果
langfuseClient.score(CreateScoreRequest.builder()
.traceId(conversationId)
.observationId(messageId)
.name("quality_score")
.value(score.getOverall())
.comment(objectMapper.writeValueAsString(score))
.build());
} catch (Exception e) {
log.error("质量评估失败", e);
}
});
}
private QualityScore performQualityEvaluation(ResponseEvaluation evaluation) {
// 这里可以集成第三方评估服务或自建评估模型
// 例如使用另一个LLM来评估回答质量
return QualityScore.builder()
.relevance(0.85)
.accuracy(0.90)
.helpfulness(0.88)
.safety(0.95)
.overall(0.89)
.evaluationMethod("llm_as_judge")
.build();
}
}
// OpenTelemetryConfig.java
@Configuration
@ConditionalOnProperty(name = "monitoring.opentelemetry.enabled", havingValue = "true")
public class OpenTelemetryConfig {
@Bean
public OpenTelemetry openTelemetry() {
return OpenTelemetrySpring.initializeOpenTelemetry(
GlobalOpenTelemetry.get()
);
}
@Bean
public Tracer tracer() {
return openTelemetry().getTracer("claude-clone");
}
}
// MetricsConfig.java
@Configuration
@EnableMetrics
public class MetricsConfig {
@Bean
public MeterRegistry meterRegistry() {
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}
@Bean
public TimedAspect timedAspect(MeterRegistry meterRegistry) {
return new TimedAspect(meterRegistry);
}
}
// ChatServiceWithMonitoring.java
@Service
public class ChatServiceWithMonitoring {
private final ChatService chatService;
private final LLMObservabilityService observabilityService;
public Flux<ChatChunk> streamChatWithMonitoring(ChatMessage message) {
// 开始追踪
TraceContext traceContext = observabilityService.startTrace("chat_request",
Map.of(
"user_id", message.getUserId(),
"conversation_id", message.getConversationId(),
"model", message.getModel(),
"message_length", message.getContent().length()
));
Instant startTime = Instant.now();
return chatService.streamChat(message)
.doOnNext(chunk -> {
// 记录流式响应事件
observabilityService.addTraceEvent(traceContext, "chunk_received",
Map.of("chunk_length", chunk.getContent().length()));
})
.doOnComplete(() -> {
// 计算响应时间
Duration duration = Duration.between(startTime, Instant.now());
observabilityService.recordLatency(message.getModel(), message.getProvider(), duration);
// 结束追踪
observabilityService.endTrace(traceContext, TraceResult.success());
// 异步质量评估
String fullResponse = reconstructFullResponse(message.getConversationId());
observabilityService.evaluateResponse(
message.getConversationId(),
message.getId(),
ResponseEvaluation.builder()
.messageId(message.getId())
.conversationId(message.getConversationId())
.build()
);
})
.doOnError(error -> {
// 记录错误
observabilityService.recordError(message.getProvider(), message.getModel(),
error.getClass().getSimpleName(), error);
observabilityService.endTrace(traceContext, TraceResult.error(error));
});
}
}
```
### 5.3 前端核心实现
#### 5.3.1 聊天组件
```tsx
// components/Chat/ChatContainer.tsx
import React, { useState, useEffect } from 'react';
import { Layout, Input, Button } from 'antd';
import { io, Socket } from 'socket.io-client';
import MessageList from './MessageList';
import ArtifactsPanel from '../Artifacts/ArtifactsPanel';
const { Content, Sider } = Layout;
export const ChatContainer: React.FC = () => {
const [socket, setSocket] = useState<Socket | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [currentMessage, setCurrentMessage] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
useEffect(() => {
const newSocket = io('/ws', {
transports: ['websocket']
});
newSocket.on('connect', () => {
console.log('Connected to server');
});
newSocket.on('message', (chunk: ChatChunk) => {
if (chunk.type === 'text') {
setMessages(prev => {
const last = prev[prev.length - 1];
if (last && last.role === 'assistant') {
return [
...prev.slice(0, -1),
{ ...last, content: last.content + chunk.content }
];
}
return [...prev, { role: 'assistant', content: chunk.content }];
});
} else if (chunk.type === 'completed') {
setIsStreaming(false);
}
});
setSocket(newSocket);
return () => {
newSocket.close();
};
}, []);
const sendMessage = () => {
if (!currentMessage.trim() || !socket) return;
const userMessage: Message = {
role: 'user',
content: currentMessage
};
setMessages(prev => [...prev, userMessage]);
setCurrentMessage('');
setIsStreaming(true);
socket.emit('chat.send', {
content: currentMessage,
conversationId: getCurrentConversationId()
});
};
return (
<Layout style={{ height: '100vh' }}>
<Content style={{ padding: '20px' }}>
<div className="chat-container">
<MessageList messages={messages} />
<div className="input-area">
<Input.TextArea
value={currentMessage}
onChange={(e) => setCurrentMessage(e.target.value)}
onPressEnter={(e) => {
if (!e.shiftKey) {
e.preventDefault();
sendMessage();
}
}}
placeholder="发送消息..."
autoSize={{ minRows: 1, maxRows: 5 }}
disabled={isStreaming}
/>
<Button
type="primary"
onClick={sendMessage}
disabled={isStreaming || !currentMessage.trim()}
>
发送
</Button>
</div>
</div>
</Content>
<Sider width={500} theme="light">
<ArtifactsPanel />
</Sider>
</Layout>
);
};
```
#### 5.3.2 Artifacts 组件
```tsx
// components/Artifacts/ArtifactsPanel.tsx
import React, { useState } from 'react';
import { Tabs, Card } from 'antd';
import MonacoEditor from '@monaco-editor/react';
import CodePreview from './CodePreview';
interface Artifact {
id: string;
type: 'code' | 'document' | 'react';
title: string;
content: string;
language?: string;
}
export const ArtifactsPanel: React.FC = () => {
const [artifacts, setArtifacts] = useState<Artifact[]>([]);
const [activeKey, setActiveKey] = useState<string>('');
return (
<div className="artifacts-panel">
<Tabs
activeKey={activeKey}
onChange={setActiveKey}
items={artifacts.map(artifact => ({
key: artifact.id,
label: artifact.title,
children: (
<div className="artifact-content">
{artifact.type === 'code' && (
<>
<MonacoEditor
height="50vh"
language={artifact.language}
value={artifact.content}
theme="vs-dark"
options={{
readOnly: false,
minimap: { enabled: false }
}}
/>
{['html', 'javascript', 'css'].includes(artifact.language || '') && (
<CodePreview code={artifact.content} language={artifact.language} />
)}
</>
)}
</div>
)
}))}
/>
</div>
);
};
```
### 5.4 配置文件
#### 5.4.1 application.yml
```yaml
spring:
application:
name: claude-clone
# 数据源配置(H2嵌入式数据库)
datasource:
url: jdbc:h2:file:./data/db/claude;MODE=PostgreSQL
driver-class-name: org.h2.Driver
username: sa
password:
# JPA配置
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
# WebSocket配置
websocket:
message-broker:
relay-host: localhost
relay-port: 61613
# 应用配置
app:
profile: development
# LLM配置
llm:
default-provider: claude
providers:
claude:
enabled: true
api-key: ${CLAUDE_API_KEY}
model: claude-3-sonnet-20240229
deepseek:
enabled: true
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com/v1
model: deepseek-chat
qwen:
enabled: true
api-key: ${QWEN_API_KEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
model: qwen-turbo
# 执行环境配置
execution:
docker:
enabled: true
resource-limits:
memory: 512m
cpu: 0.5
timeout: 30s
frontend:
enabled: true
# 存储配置
storage:
file:
path: ./data/files
cache:
type: caffeine
caffeine:
max-size: 1000
expire-after-write: 1h
# 分析工具配置
analysis:
engine: local-python
python:
executable: python3
timeout: 60s
# 监控和可观测性配置
monitoring:
# Langfuse 配置
langfuse:
enabled: true
api-key: ${LANGFUSE_API_KEY}
host: ${LANGFUSE_HOST:https://cloud.langfuse.com}
project: claude-clone
sampling-rate: 1.0
# OpenTelemetry 配置
opentelemetry:
enabled: true
service-name: claude-clone
tracer:
jaeger:
endpoint: ${JAEGER_ENDPOINT:http://localhost:14268/api/traces}
# Prometheus 指标配置
prometheus:
enabled: true
port: 9090
path: /metrics
# 质量评估配置
evaluation:
enabled: true
evaluator: llm-as-judge
evaluation-model: claude-3-haiku-20240307
evaluation-prompt: |
请对以下AI助手的回答进行评分(0-1分):
用户问题:{question}
AI回答:{answer}
评分维度:
1. 相关性(0-1):回答是否与问题相关
2. 准确性(0-1):回答中的信息是否准确
3. 有用性(0-1):回答是否对用户有帮助
4. 安全性(0-1):回答是否安全无害
请以JSON格式返回评分结果。
# 业务指标配置
business-metrics:
enabled: true
track-user-satisfaction: true
track-conversion-rate: true
track-task-completion: true
custom-events:
- code-execution
- file-upload
- artifact-creation
# 告警配置
alerts:
enabled: true
channels:
- type: webhook
url: ${ALERT_WEBHOOK_URL}
- type: email
recipients: ${ALERT_EMAIL_RECIPIENTS}
rules:
- name: high-error-rate
condition: error_rate > 0.05
duration: 5m
- name: high-latency
condition: p95_latency > 10s
duration: 3m
- name: low-quality-score
condition: avg_quality_score < 0.7
duration: 10m
```
#### 5.4.2 pom.xml
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.claude</groupId>
<artifactId>claude-clone</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<langchain4j.version>0.24.0</langchain4j.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- LangChain4j -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- Docker客户端 -->
<dependency>
<groupId>com.spotify</groupId>
<artifactId>docker-client</artifactId>
<version>8.16.0</version>
</dependency>
<!-- 监控和可观测性 -->
<!-- Langfuse -->
<dependency>
<groupId>com.langfuse</groupId>
<artifactId>langfuse-java</artifactId>
<version>2.1.0</version>
</dependency>
<!-- OpenTelemetry -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.32.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.32.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
<version>1.32.0-alpha</version>
</dependency>
<!-- Micrometer + Prometheus -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
```
### 5.5 部署和运行
#### 5.5.1 启动脚本
```bash
#!/bin/bash
# start.sh
# 检查环境
check_requirements() {
echo "检查运行环境..."
# 检查Java
if ! command -v java &> /dev/null; then
echo "错误:未安装Java,请安装Java 17+"
exit 1
fi
# 检查Docker
if ! command -v docker &> /dev/null; then
echo "错误:未安装Docker,请安装Docker Desktop"
exit 1
fi
# 检查Python(可选)
if ! command -v python3 &> /dev/null; then
echo "警告:未安装Python3,Analysis功能将不可用"
fi
echo "环境检查通过!"
}
# 准备环境
prepare_environment() {
echo "准备运行环境..."
# 创建数据目录
mkdir -p ./data/{db,files,logs}
# 预下载Docker镜像
echo "预下载代码执行环境镜像..."
docker pull python:3.11-alpine &
docker pull node:20-alpine &
docker pull openjdk:17-alpine &
wait
echo "环境准备完成!"
}
# 设置环境变量
setup_env() {
echo "配置环境变量..."
# 检查API Keys
if [ -z "$CLAUDE_API_KEY" ]; then
echo "警告:未设置CLAUDE_API_KEY"
fi
if [ -z "$DEEPSEEK_API_KEY" ]; then
echo "警告:未设置DEEPSEEK_API_KEY"
fi
if [ -z "$QWEN_API_KEY" ]; then
echo "警告:未设置QWEN_API_KEY"
fi
}
# 启动后端
start_backend() {
echo "启动后端服务..."
cd backend
./mvnw spring-boot:run &
BACKEND_PID=$!
cd ..
echo "后端服务已启动,PID: $BACKEND_PID"
}
# 启动前端
start_frontend() {
echo "启动前端服务..."
cd frontend
npm install
npm run dev &
FRONTEND_PID=$!
cd ..
echo "前端服务已启动,PID: $FRONTEND_PID"
}
# 主函数
main() {
check_requirements
prepare_environment
setup_env
start_backend
sleep 10 # 等待后端启动
start_frontend
echo ""
echo "========================================="
echo "Claude Clone 已启动!"
echo "前端地址:http://localhost:5173"
echo "后端地址:http://localhost:8080"
echo "H2控制台:http://localhost:8080/h2-console"
echo "========================================="
echo ""
echo "按 Ctrl+C 停止所有服务"
# 等待退出信号
trap "kill $BACKEND_PID $FRONTEND_PID; exit" INT
wait
}
main
```
#### 5.5.2 Docker Compose(可选)
```yaml
version: '3.8'
services:
app:
build:
context: .
dockerfile: docker/Dockerfile
ports:
- "8080:8080"
- "5173:5173"
volumes:
- ./data:/app/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- CLAUDE_API_KEY=${CLAUDE_API_KEY}
- DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY}
- QWEN_API_KEY=${QWEN_API_KEY}
depends_on:
- init-images
init-images:
image: docker:latest
command: |
sh -c "
docker pull python:3.11-alpine
docker pull node:20-alpine
docker pull openjdk:17-alpine
"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
```
## 6. LLM 监控和可观测性方案
### 6.1 监控架构设计
基于之前的调研,我们采用 **Langfuse + OpenTelemetry** 的组合方案,实现从业务效果角度对AI应用进行全面监控:
```mermaid
graph TB
A[用户请求] --> B[Chat Controller]
B --> C[Monitoring Interceptor]
C --> D[LLM Provider]
D --> E[AI Model API]
C --> F[Langfuse Client]
C --> G[OpenTelemetry Tracer]
C --> H[Micrometer Registry]
F --> I[Langfuse Dashboard]
G --> J[Jaeger/Zipkin]
H --> K[Prometheus]
K --> L[Grafana Dashboard]
subgraph "质量评估"
M[Quality Evaluator]
N[LLM-as-Judge]
O[Business Metrics]
end
F --> M
M --> N
M --> O
subgraph "告警系统"
P[Alert Rules]
Q[Webhook/Email]
end
K --> P
P --> Q
```
### 6.2 核心监控指标
#### 6.2.1 技术指标
- **延迟指标**:P50/P95/P99响应时间
- **吞吐量**:每秒请求数(RPS)、并发用户数
- **错误率**:HTTP错误、API错误、超时错误
- **资源使用**:Token消耗、API调用成本
#### 6.2.2 质量指标
- **准确性**:回答的正确性评分
- **相关性**:回答与问题的相关度
- **有用性**:对用户的帮助程度
- **安全性**:内容安全检查结果
- **幻觉检测**:虚假信息识别
#### 6.2.3 业务指标
- **用户满意度**:用户反馈评分
- **任务完成率**:用户目标达成率
- **会话质量**:对话轮次、用户留存
- **功能使用**:Artifacts使用率、代码执行成功率
### 6.3 监控实施策略
#### 6.3.1 三层监控架构
**第一层:数据采集**
- 在LLM Provider抽象层注入监控点
- 收集请求/响应数据、用户行为、系统指标
- 支持采样配置,避免性能影响
**第二层:数据处理**
- 实时指标计算和异常检测
- 异步质量评估(LLM-as-Judge)
- 业务指标聚合和分析
**第三层:展示决策**
- 多维度仪表板展示
- 智能告警和异常通知
- 趋势分析和优化建议
#### 6.3.2 监控数据流
```
用户请求 -> 拦截器 -> 追踪开始
|
v
LLM调用 -> 指标记录 -> Langfuse/OpenTel
|
v
响应返回 -> 质量评估 -> 异步处理
|
v
用户反馈 -> 业务指标 -> 长期分析
```
### 6.4 方案选择理由
#### 6.4.1 为什么选择 Langfuse?
1. **LLM专用**:专门为LLM应用设计,理解AI应用特点
2. **开源可控**:可以自托管,数据安全可控
3. **Java支持**:提供Java SDK,集成简单
4. **功能全面**:支持流式追踪、成本分析、质量评估
5. **社区活跃**:持续更新,社区支持良好
#### 6.4.2 为什么结合 OpenTelemetry?
1. **标准化**:业界标准,确保兼容性和可移植性
2. **生态丰富**:支持多种后端,如Jaeger、Zipkin
3. **Spring集成**:与Spring Boot无缝集成
4. **厂商中立**:避免供应商锁定
#### 6.4.3 为什么使用 LLM-as-Judge?
1. **自动化**:无需人工评估,可以实时进行
2. **一致性**:评估标准统一,结果可重复
3. **可扩展**:可以根据业务需求调整评估维度
4. **成本效益**:使用cheaper模型评估expensive模型
### 6.5 实施建议
#### 6.5.1 分阶段实施
**第一阶段**(1周):
- 集成Langfuse基础追踪
- 配置基本技术指标监控
- 设置简单的错误告警
**第二阶段**(1周):
- 添加OpenTelemetry链路追踪
- 配置Prometheus+Grafana仪表板
- 实现质量评估系统
**第三阶段**(持续):
- 优化告警规则
- 添加业务指标
- 持续改进评估模型
#### 6.5.2 运维建议
1. **数据保留**:设置合理的数据保留期限,平衡存储成本和分析需求
2. **性能影响**:监控采样率配置,避免影响主业务性能
3. **告警疲劳**:精心设计告警规则,避免误报
4. **团队培训**:让团队熟悉监控工具和数据解读
## 7. 开发路线图
### 7.1 第一阶段:MVP(2周)
- [x] 基础项目搭建
- [x] 简单的聊天界面
- [x] Claude API集成
- [x] 消息流式传输
- [x] 基础的会话管理
### 7.2 第二阶段:核心功能(4周)
- [ ] 多模型支持(DeepSeek、Qwen)
- [ ] Artifacts功能
- [ ] 前端代码执行
- [ ] 代码高亮和编辑器
- [ ] 会话持久化
### 7.3 第三阶段:高级功能(4周)
- [ ] Docker代码执行
- [ ] Analysis Tool基础版
- [ ] 文件上传和处理
- [ ] 用户认证系统
- [ ] 多会话管理
### 7.4 第四阶段:优化和扩展(持续)
- [ ] 性能优化
- [ ] 更多语言支持
- [ ] 高级Analysis功能
- [ ] 插件系统
- [ ] 企业级功能
### 7.5 第五阶段:监控和可观测性(2周)
- [ ] Langfuse集成和基础追踪
- [ ] OpenTelemetry配置和链路追踪
- [ ] Prometheus指标收集和Grafana仪表板
- [ ] 质量评估系统(LLM-as-Judge)
- [ ] 告警规则配置
- [ ] 业务指标追踪
- [ ] 用户反馈收集机制
## 8. 总结
本技术方案提供了一个完整的Claude.ai克隆实现路径,具有以下特点:
1. **轻量级设计**:可在单机MacBook上运行
2. **良好的抽象**:便于后续扩展和替换组件
3. **渐进式开发**:从MVP开始,逐步完善功能
4. **技术成熟度高**:基于Spring Boot和React等成熟技术栈
5. **易于部署**:提供一键启动脚本和Docker支持
6. **全面监控**:集成Langfuse和OpenTelemetry的企业级监控方案
7. **业务导向**:从业务效果角度监控AI应用表现和用户体验
### 8.1 监控方案优势
**技术优势**:
- **标准化**:采用OpenTelemetry业界标准,确保兼容性
- **专业化**:Langfuse专为LLM应用设计,功能更贴合需求
- **可扩展**:支持自定义指标和评估规则
- **低侵入**:通过抽象层设计,监控逻辑与业务逻辑分离
**业务价值**:
- **质量保证**:自动化质量评估,及时发现问题
- **成本控制**:Token使用量和成本监控,优化资源配置
- **用户体验**:响应时间和错误率监控,提升用户满意度
- **数据驱动**:业务指标分析,支持产品迭代决策
**运维效率**:
- **自动告警**:异常情况第一时间通知
- **可视化**:直观的仪表板和报表
- **追踪调试**:完整的请求链路追踪,快速定位问题
- **历史分析**:长期数据积累,支持趋势分析
通过遵循这个方案,您可以快速构建一个功能完善的AI对话应用,并具备企业级的监控和可观测性能力,为业务优化和产品迭代提供数据支撑。