这次开发主要围绕小程序中的“首次登录建档”功能展开。目标不是单纯做一个问卷,而是让用户第一次使用时,通过类似正常 AI 对话的方式回答几个和便秘情况相关的问题,再由后端调用 AI 对回答进行整理,最终形成结构化档案和 Markdown 个性化资料,供后续 AI 回复时参考。

整个功能涉及小程序端、后端、管理端和数据库,算是一次比较完整的前后端联动开发。

一、需求分析

本次功能最开始的目标比较明确:当用户第一次登录小程序时,需要自动完成一次基础情况摸排。用户回答后,系统要把这些回答整理成可以长期使用的用户档案。

具体拆分后,主要有几个关键点:

  1. 判断用户是否需要建档。
  2. 小程序端引导用户完成建档问答。
  3. 支持文字回答和语音回答。
  4. 后端调用 AI 对用户回答进行结构化整理。
  5. 将原始回答和整理后的档案保存到数据库。
  6. 生成一份 Markdown 用户档案,供后续 AI 回复读取。
  7. 管理端提供问卷题目管理和用户档案查看能力。
  8. 开发阶段需要方便查看云端数据库中写入的数据。

这里比较重要的一点是:用户回答往往不是标准化的,可能会比较口语化,比如“好几天才一次吧”“喝水不多”“有时候肚子胀”。如果直接存原文,后续 AI 很难稳定使用,所以必须引入 AI 整理这一层。

二、功能迭代

一开始设计时,我采用的是比较传统的“弹窗问卷”形式:用户第一次进入小程序后,弹出一个问卷弹框,用户逐题填写。

但实际思考和测试后发现,这种形式和小程序原本的 AI 对话体验不太一致。用户使用的是 AI 健康助手,如果突然出现一个独立问卷弹窗,会有一点割裂感。

后来将方案调整为:直接使用已有 AI 对话框完成建档问答

也就是说,建档问题由 AI 助手以聊天消息的形式逐条提出,用户仍然使用底部正常输入框进行回答。这样体验更统一,也更符合“AI 助手在和用户聊天”的感觉。

小程序端的核心逻辑大致是:

async checkOnboarding() {
  try {
    const status = await onboardingApi.status()
    if (!status.needsOnboarding) {
      return
    }

    const questions = await onboardingApi.questions()
    if (!questions.length) {
      return
    }

    this.startOnboardingConversation(questions, false)
  } catch (error) {
    console.error('check onboarding failed', error)
  }
}

当后端返回 needsOnboarding=true 时,小程序端就开始建档对话。

startOnboardingConversation(questions: OnboardingQuestion[], forceNew = false) {
  const firstQuestion = questions[0]
  const content = `为了让后续建议更贴合您的情况,我先问几个便秘相关的小问题。\n\n${firstQuestion.questionText}`

  this.setData({
    showOnboarding: true,
    showWelcome: false,
    onboardingQuestions: questions,
    onboardingStep: 0,
    onboardingCurrentQuestion: firstQuestion,
    onboardingAnswers: [],
    onboardingForceNew: forceNew,
    messages: [...this.data.messages, this.createLocalMessage('assistant', content)]
  })

  this.updateViewStates()
  this.scrollToBottom()
}

这个设计的好处是,不需要为建档单独设计一套复杂页面,而是复用已有聊天界面,把建档作为一种特殊对话状态来处理。

三、小程序端实现

小程序端主要做了三件事:

  1. 登录后检查是否需要建档。
  2. 使用聊天消息逐题提问。
  3. 用户回答后收集答案并提交。

在发送消息时,如果当前处于建档状态,就不走普通 AI 问答流程,而是进入建档回答处理逻辑:

async sendMessage() {
  const content = this.data.inputText.trim()
  if (!content || this.data.isLoading) return

  if (this.data.showOnboarding) {
    await this.handleOnboardingReply(content)
    return
  }

  // 普通 AI 对话逻辑
}

建档回答处理逻辑会保存当前问题的回答,然后判断是否还有下一题:

async handleOnboardingReply(content: string) {
  const question = this.data.onboardingCurrentQuestion
  if (!question || this.data.onboardingSubmitting) return

  const nextAnswers = this.data.onboardingAnswers
    .filter((item) => item.questionId !== question.id)
    .concat([{
      questionId: question.id,
      questionText: question.questionText,
      answerText: content
    }])

  const nextStep = this.data.onboardingStep + 1

  if (nextStep < this.data.onboardingQuestions.length) {
    const nextQuestion = this.data.onboardingQuestions[nextStep]
    this.setData({
      inputText: '',
      onboardingAnswers: nextAnswers,
      onboardingStep: nextStep,
      onboardingCurrentQuestion: nextQuestion,
      messages: [
        ...this.data.messages,
        this.createLocalMessage('user', content),
        this.createLocalMessage('assistant', nextQuestion.questionText)
      ]
    })
    return
  }

  await this.submitOnboardingAnswers(nextAnswers)
}

为了方便开发测试,我还在小程序端加了一个“测试新建档案”的入口。因为如果每次测试都要重新注册账号,效率会很低。这个按钮可以强制开始一轮新的建档流程,方便反复测试提交、数据库写入和管理端展示。

四、后端接口设计

后端新增了建档相关接口,主要包括:

GET  /onboarding/status
GET  /onboarding/questions
POST /onboarding/submit

其中 /onboarding/status 用来判断当前用户是否已经完成建档。登录接口中也会返回 needsOnboarding,小程序可以据此决定是否触发建档流程。

登录响应中增加了类似这样的逻辑:

private Map<String, Object> buildLoginResponse(AppUser user) {
    Map<String, Object> data = new HashMap<>();
    data.put("token", jwtUtils.generateUserToken(String.valueOf(user.getId()), user.getUsername()));
    data.put("profile", buildProfile(user));
    data.put("needsOnboarding", !onboardingService.hasCompletedProfile(String.valueOf(user.getId())));
    return data;
}

判断是否完成建档的逻辑放在 OnboardingService 中:

public boolean hasCompletedProfile(String userId) {
    if (!StringUtils.hasText(userId)) {
        return false;
    }

    Long count = userOnboardingProfileMapper.selectCount(
        new LambdaQueryWrapper<UserOnboardingProfile>()
            .eq(UserOnboardingProfile::getUserId, userId)
            .eq(UserOnboardingProfile::getStatus, 1)
    );

    return count != null && count > 0;
}

这样“第一次登录”的判断不是简单依赖注册时间,而是依赖用户是否已经有完成状态的建档档案。这个方式更灵活,比如后续如果需要重新建档,也比较容易扩展。

五、数据库设计

这次建档功能新增了三张表。

第一张是问卷题目表:

CREATE TABLE IF NOT EXISTS `onboarding_question` (
    `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '建档题目ID',
    `question_text` VARCHAR(500) NOT NULL COMMENT '题目内容',
    `question_type` VARCHAR(32) DEFAULT 'text' COMMENT '题目类型',
    `required_flag` TINYINT DEFAULT 1 COMMENT '是否必填',
    `sort_order` INT DEFAULT 0 COMMENT '排序',
    `status` TINYINT DEFAULT 1 COMMENT '状态 1-启用 0-停用',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted` TINYINT DEFAULT 0 COMMENT '逻辑删除'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='首次建档问卷题目表';

第二张是用户结构化档案表:

CREATE TABLE IF NOT EXISTS `user_onboarding_profile` (
    `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '建档档案ID',
    `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
    `basic_summary` TEXT COMMENT '基础情况摘要',
    `bowel_habit_summary` TEXT COMMENT '排便习惯摘要',
    `diet_water_summary` TEXT COMMENT '饮食饮水摘要',
    `medication_summary` TEXT COMMENT '用药情况摘要',
    `risk_factors` TEXT COMMENT '风险关注点',
    `personalization_guide` TEXT COMMENT '个性化回答指导',
    `md_content` LONGTEXT COMMENT 'Markdown档案内容',
    `status` TINYINT DEFAULT 1 COMMENT '状态 1-已完成 0-未完成',
    `completed_time` DATETIME COMMENT '完成时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户首次建档档案表';

第三张是原始问答明细表:

CREATE TABLE IF NOT EXISTS `user_onboarding_answer` (
    `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '建档回答ID',
    `profile_id` BIGINT NOT NULL COMMENT '建档档案ID',
    `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
    `question_id` BIGINT COMMENT '题目ID',
    `question_text` VARCHAR(500) COMMENT '题目内容',
    `answer_text` TEXT COMMENT '文字回答',
    `audio_text` TEXT COMMENT '语音转写文本',
    `audio_id` VARCHAR(64) COMMENT '音频ID'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户首次建档问答明细表';

这里我没有只保存 AI 整理后的结果,而是同时保存了原始问答。这样做有两个原因:

第一,AI 整理结果可能会有偏差,保留原始回答便于之后人工检查。

第二,后续如果优化了 AI 提示词,可以基于原始回答重新生成结构化档案。

六、AI整理与工具调用

本次功能的核心不是简单保存问卷,而是要让 AI 把用户回答整理成结构化内容。

后端在收到用户回答后,会构造 prompt,让 AI 输出严格 JSON:

private String buildFormatPrompt(List<OnboardingAnswerSubmitDTO> answers) {
    StringBuilder builder = new StringBuilder();
    builder.append("请把老年人便秘小程序首次建档问卷整理为严格JSON,不要输出解释文字。")
            .append("你必须输出一个工具调用对象,格式为:")
            .append("{\"action\":\"SAVE_ONBOARDING_PROFILE\",\"payload\":{\"basicSummary\":\"\",\"bowelHabitSummary\":\"\",\"dietWaterSummary\":\"\",\"medicationSummary\":\"\",\"riskFactors\":\"\",\"personalizationGuide\":\"\"}}。\n")
            .append("要求:中文、简洁、适合后续AI个性化回答使用;若信息缺失请写“未提及”。\n\n问答如下:\n");

    for (OnboardingAnswerSubmitDTO answer : answers) {
        builder.append("问题:").append(blankToDefault(answer.getQuestionText(), "未命名问题")).append("\n")
                .append("回答:").append(resolveAnswerText(answer)).append("\n\n");
    }

    return builder.toString();
}

AI 返回后,后端会解析 JSON,然后执行内部工具动作:

public Object executeAgentTool(String action, Map<String, Object> payload) {
    if (SAVE_ONBOARDING_PROFILE.equals(action)) {
        return saveProfileByTool(payload);
    }
    return Map.of("action", action, "message", "Unsupported action");
}

这里的 SAVE_ONBOARDING_PROFILE 相当于一个内部工具调用动作。AI 负责把用户非结构化回答整理成结构化字段,真正的数据库写入仍然由后端 Service 控制。

这样设计比较安全,也比较符合后端分层:AI 不直接操作数据库,而是输出结构化意图,由后端执行。

保存档案时,会同时写入结构化字段、原始问答和 Markdown 内容:

private UserOnboardingProfile saveProfileByTool(Map<String, Object> payload) {
    String userId = String.valueOf(payload.getOrDefault("userId", ""));

    UserOnboardingProfile profile = new UserOnboardingProfile();
    profile.setUserId(userId);
    profile.setBasicSummary(valueOf(payload, "basicSummary"));
    profile.setBowelHabitSummary(valueOf(payload, "bowelHabitSummary"));
    profile.setDietWaterSummary(valueOf(payload, "dietWaterSummary"));
    profile.setMedicationSummary(valueOf(payload, "medicationSummary"));
    profile.setRiskFactors(valueOf(payload, "riskFactors"));
    profile.setPersonalizationGuide(valueOf(payload, "personalizationGuide"));
    profile.setStatus(1);
    profile.setCompletedTime(LocalDateTime.now());
    profile.setMdContent(buildMarkdown(profile));

    userOnboardingProfileMapper.insert(profile);
    saveAnswers(userId, profile.getId(), payload.get("answers"));
    writeMarkdownFile(userId, profile.getMdContent());

    return userOnboardingProfileMapper.selectById(profile.getId());
}

如果 AI 调用失败,系统也不会直接中断,而是用本地规则兜底生成基础档案。这一点在实际开发中很重要,因为 AI 服务不是百分百稳定的,核心业务流程要尽量保证能完成。

七、Markdown用户档案

为了让后续 AI 回复更个性化,后端会生成一份 Markdown 档案。

大致格式如下:

private String buildMarkdown(UserOnboardingProfile profile) {
    return "# 用户便秘健康建档\n\n"
            + "## 基础摘要\n" + blankToDefault(profile.getBasicSummary(), "未提及") + "\n\n"
            + "## 排便习惯\n" + blankToDefault(profile.getBowelHabitSummary(), "未提及") + "\n\n"
            + "## 饮食饮水\n" + blankToDefault(profile.getDietWaterSummary(), "未提及") + "\n\n"
            + "## 用药情况\n" + blankToDefault(profile.getMedicationSummary(), "未提及") + "\n\n"
            + "## 风险关注\n" + blankToDefault(profile.getRiskFactors(), "未提及") + "\n\n"
            + "## 后续AI回答指导\n" + blankToDefault(profile.getPersonalizationGuide(), "未提及") + "\n";
}

Markdown 内容一方面存入数据库的 md_content 字段,另一方面写入后端本地文件:

private void writeMarkdownFile(String userId, String content) {
    try {
        Path path = resolveProfilePath(userId);
        Files.createDirectories(path.getParent());
        Files.writeString(path, content, StandardCharsets.UTF_8);
    } catch (Exception e) {
        log.warn("写入用户建档Markdown失败: {}", e.getMessage());
    }
}

后续 AI 对话时,会根据用户 ID 读取这份档案,并追加到 System Prompt 中。这样 AI 在回答时就能知道用户的排便习惯、饮食饮水情况、用药情况和风险点,而不是每次都从零开始。

八、管理员端功能

管理端主要新增了两个页面。

第一个是问卷题目管理页面,用来维护建档问题,包括:

  • 新增题目
  • 编辑题目
  • 删除题目
  • 启用/停用题目
  • 设置排序
  • 设置是否必填

第二个是用户档案查看页面,用来查看用户提交后的建档信息,包括:

  • 用户基本信息
  • 建档完成时间
  • 结构化字段
  • 原始问答
  • Markdown 内容预览

由于数据库已经部署到云端,开发时不能直接查看数据库内容,所以管理端还增加了一个调试入口,用来查看建档相关三张表的数据。这只是开发阶段的辅助功能,后续正式上线时可以移除或隐藏。

九、总结收获

目前建档功能已经完成了主要闭环,但还有一些可以继续优化的地方:

第一,管理端可以增加“原始回答”和“AI 结构化结果”的对照展示。这样可以更直观看出 AI 是否理解正确。

第二,支持管理员手动修订档案。因为 AI 整理结果不一定百分百准确,如果管理员可以修改结构化字段和 Markdown,会更适合后续运营。

第三,支持重新调用 AI 整理档案。后续如果优化了 prompt,可以基于已有原始问答重新生成档案,而不用用户重新填写。

这次首次登录建档功能,不只是增加了几个问卷问题,而是完成了一条比较完整的业务链路:

用户首次登录 → 小程序对话式提问 → 用户文字或语音回答 → 后端调用 AI 整理 → 工具动作保存数据库 → 生成 Markdown 用户档案 → 后续 AI 回复读取个性化信息 → 管理端查看和维护。

通过这次开发,我对前后端联调、AI 结构化处理、数据库设计、管理端调试工具以及用户体验迭代都有了更完整的理解。

这个功能后续还可以继续完善,但目前已经为个性化 AI 健康建议打下了基础。后面如果继续扩展用户画像、风险识别和长期健康管理,这个建档模块会成为很重要的入口。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐