一、背景

在我们的智能问答系统初期版本中,日志记录仅能捕获AI的回答内容。随着需求发展,我们需要:
1. 完整记录用户提问和AI回答的全流程对话
2. 明确区分消息来源(用户/AI)
3. 支持结构化存储和分析

二、技术实现路径

第一阶段:基础日志记录

初始版本只能记录AI的流式响应:

日志文件数据结构定义:

package com.dpsk.dpsk_quiz_sys_java.pojo.dto;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;

public class EventRecord {
    private final long timestamp;
    private final String eventId;
    private final String eventType;
    private final String rawData;
    private final String content;

    public EventRecord(long timestamp, String eventId, String eventType, String rawData, String content) {
        this.timestamp = timestamp;
        this.eventId = eventId;
        this.eventType = eventType;
        this.rawData = rawData;
        this.content = content;
    }

    // Getters
    public long getTimestamp() {
        return timestamp;
    }

    public String getEventId() {
        return eventId;
    }

    public String getEventType() {
        return eventType;
    }

    public String getRawData() {
        return rawData;
    }

    public String getContent() {
        return content;
    }

    // 格式化为可读字符串
    public String toLogString() {
        LocalDateTime time = LocalDateTime.ofInstant(
                Instant.ofEpochMilli(timestamp),
                ZoneId.systemDefault()
        );
        return String.format("[%s] ID: %s, Type: %s, Content: %s",
                time, eventId, eventType, content);
    }

    // 可选:添加 Jackson 注解如果需要序列化
    @Override
    public String toString() {
        return "EventRecord{" +
                "timestamp=" + timestamp +
                ", eventId='" + eventId + '\'' +
                ", eventType='" + eventType + '\'' +
                ", content='" + content + '\'' +
                '}';
    }
}

主要方法:

 // 记录完整交互日志
    private void logCompleteInteraction(List<EventRecord> records) {
        StringBuilder logBuilder = new StringBuilder("\n===== Deepseek 完整交互记录 =====\n");
        logBuilder.append(String.format("共收到 %d 个事件:\n", records.size()));

//        for (EventRecord record : records) {
//            logBuilder.append(record.toString()).append("\n");
//        }

        // 提取并拼接所有内容
        String fullResponse = records.stream()
                .map(EventRecord::getContent)
                .collect(Collectors.joining());

        logBuilder.append("\n完整响应内容:\n").append(fullResponse);
        logBuilder.append("\n===== 交互结束 =====\n");

        logger.info(logBuilder.toString());
    }
}

缺陷

  • 丢失用户提问上下文

  • 无法关联问答对

  • 流式响应被分散记录

第二阶段:结构化设计

1.数据模型定义:

package com.dpsk.dpsk_quiz_sys_java.pojo.dto;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;

public class ResponseRecord {
    private final String id;
    private final int type; // 0-AI回答 / 1-用户提问
    private final long timestamp;
    private final String content;
    private final String rawData; // 原始数据

    // 类型常量
    public static final int TYPE_AI_RESPONSE = 0;
    public static final int TYPE_USER_QUERY = 1;

    public ResponseRecord(String id, int type, long timestamp, String content, String rawData) {
        this.id = id;
        this.type = type;
        this.timestamp = timestamp;
        this.content = content;
        this.rawData = rawData;
    }

    // Getters
    public String getId() { return id; }
    public int getType() { return type; }
    public long getTimestamp() { return timestamp; }
    public String getContent() { return content; }
    public String getRawData() { return rawData; }

    public String getFormattedTime() {
        return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())
                .toString();
    }

    @Override
    public String toString() {
        return String.format("[%s] %s | ID: %s\nContent: %s\nRaw: %s",
                getFormattedTime(),
                type == TYPE_AI_RESPONSE ? "AI Response" : "User Query",
                id,
                content,
                rawData);
    }
}

2.后端改造:

List<Map<String, String>> inputMessages = JsonUtils.parseJsonList(messages);
        List<ResponseRecord> userQueries = inputMessages.stream()
                .filter(msg -> "user".equals(msg.get("role")))
                .map(msg -> new ResponseRecord(
                        UUID.randomUUID().toString(),
                        ResponseRecord.TYPE_USER_QUERY, // 1
                        System.currentTimeMillis(),
                        (String) msg.get("content"),
                        JsonUtils.convertObj2Json(msg) // 原始数据
                ))
                .collect(Collectors.toList());

        logUserQueries(userQueries);

 接受前端返回的message用于记录用户提问。

@Override
            public void onEvent(EventSource eventSource, String id, String type, String data) {
                if (DONE.equals(data)) {
                    return;
                }
                String content = getContent(data);

                // 记录AI响应(type=0)
                responseRecords.add(new ResponseRecord(
                        id,
                        ResponseRecord.TYPE_AI_RESPONSE, // 0
                        System.currentTimeMillis(),
                        content,
                        data
                ));

                pw.write("data:" + JsonUtils.convertObj2Json(new ContentDto(content)) + "\n\n");
                pw.flush();
            }

在后端响应的同时,记录ai响应的内容,到日志文件中。

3.日志记录方法:

//日志记录方法
    private void logUserQueries(List<ResponseRecord> userQueries) {
        if (userQueries.isEmpty()) return;

        StringBuilder log = new StringBuilder("\n===== 用户提问记录 =====\n");
        userQueries.forEach(query ->
                log.append(query.toString()).append("\n\n"));
        log.append("共收到 ").append(userQueries.size()).append(" 条用户提问");

        logger.info(log.toString());
    }

    private void logCompleteInteraction(List<ResponseRecord> aiResponses) {
        StringBuilder log = new StringBuilder("\n===== AI响应记录 =====\n");
//        aiResponses.forEach(response ->
//                log.append(response.toString()).append("\n\n"));

        // 统计信息
        String fullContent = aiResponses.stream()
                .map(ResponseRecord::getContent)
                .collect(Collectors.joining());

        log.append("完整响应内容:\n").append(fullContent)
                .append("\n共生成 ").append(aiResponses.size()).append(" 条响应片段");

        logger.info(log.toString());
    }
}

实现分层日志记录。

输出效果:

三、主要收获:

  1. 类型显式声明优于隐式判断

  2. 前端标记+后端验证的双重保障

  3. 结构化日志显著提升可观测性

  4. 上下文保存使调试更高效

四、延伸思考

未来可扩展方向:

  1. 将会话ID贯穿全链路

  2. 增加情感分析标记

  3. 实现基于类型的响应策略

  4. 持久化存储到数据库(关键目标)

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐