AI代码生成平台

liyupi/yu-ai-code-mother

JavaTypeScript研发与代码执行

项目目录

项目简介

AI代码生成平台支持通过自然语言生成 HTML 页面、多文件网页和 Vue 工程。用户可以预览生成结果,选中页面元素继续提出修改要求,并将应用构建发布为可分享的网站。

如何写进简历

页匠 · 品牌活动页面生成平台 | 场景化示例

项目描述:基于 AI 代码生成平台,面向品牌运营,将新品卖点、主视觉与活动时间生成展示页,支持对话改稿、预览和发布。

技术栈:Java、Spring Boot、LangChain4j、Vue 3、MySQL、Redis

主要工作:

  • 分类型生成与流式交互:基于 LangChain4j 按产物类型路由 HTML、多文件与 Vue 生成链路,将模型文本和文件工具调用统一转换为 SSE 事件,支持活动页流式生成与连续修改。
  • 应用上下文与服务复用:以 appId 关联活动页与对话,复用 MySQL 历史、Redis 窗口记忆和 Caffeine 服务缓存,支持连续改稿及服务实例重建后加载近期对话。
  • 真实构建与产物发布:利用现有部署链路,对 Vue 活动页执行依赖安装和生产构建,检查命令结果与 dist 后发布产物,以 deployKey 关联访问地址和部署记录。

面试题与回答

  1. 大模型流式输出与工具调用之间如何保持状态一致?

    展开收起

    核心回答

    AI 代码生成平台把普通代码文本流和带工具调用的 Vue 工程生成分开处理。HTML、多文件模式边展示边收集文本,流结束后统一解析并保存;Vue 模式使用 LangChain4j 的 TokenStream,分别处理文本片段、工具请求和工具执行回调,再转换成页面可读内容,通过 SSE 推送。

    这个版本的关键是:部分工具请求只驱动展示,真正执行发生在完整模型响应形成之后。 工具结果按原请求写入模型记忆,再继续调用模型。展示层按工具 ID 避免重复提示,但这还不是完整的执行幂等机制;流结束也不代表工程一定构建成功。

    聊天请求经过哪些层

    AppController 接收应用 ID 和用户消息,调用 AppServiceImpl.chatToGenCode();服务层校验应用与用户,先记录用户消息,再进入 AiCodeGeneratorFacade.generateAndSaveCodeStream(),按生成类型选择对应的 LangChain4j 服务。这里的聊天链路直接进入模型和工具循环,不需要经过另一个 LangGraph4j 工作流。

    HTML 和多文件模式返回 Flux<String>。门面类收集全部文本,在正常结束时调用代码解析器和文件保存器;简单流处理器再收集展示文本,写入聊天记录。这条路线没有把每个文本片段当成一次文件写入。

    Vue 模式则把三类回调转换成内部 JSON 消息:AI_RESPONSE 携带模型文本,TOOL_REQUEST 携带工具请求信息,TOOL_EXECUTED 携带执行请求和返回结果。随后交给 JsonMessageStreamHandler,处理器把它们转成展示字符串,控制器再以 {"d": "展示片段"} 包装成 SSE 数据,并在正常结束后追加 done 事件。浏览器接收的不是这三类原始工具消息的直接透传。

    部分工具回调为什么不会直接执行

    项目内包含自定义的 dev/langchain4j 实现,回调语义需要沿这层看。在 OpenAiStreamingChatModel 中,工具片段到达时会保留调用 ID、名称并追加参数;传给 onPartialToolExecutionRequest() 的 arguments 是本次参数片段,不是已经拼好的完整参数。对应实现说明了这一点。

    模型响应收集完成后,AiServiceStreamingResponseHandler.onCompleteResponse() 读取完整 AiMessage 中的工具请求,逐个交给 ToolExecutor。执行返回后,先把对应的 ToolExecutionResultMessage 加入记忆,再触发 onToolExecuted(),并带着更新后的消息请求下一轮模型;模型不再要求调用工具时,才回调应用层的最终完成处理。完整请求执行分支与部分回调是两条不同职责。

    所以,参数分成多块时,应用层只是不断获知“这个工具正在形成请求”,不会由展示处理器拿半段参数重复执行工具。

    展示状态与真实执行还有哪些区别

    JsonMessageStreamHandler 在一次流处理内维护 seenToolIds。首次看到某个工具 ID 时,返回该工具的调用提示,后续同 ID 的请求片段返回空字符串,避免页面反复展示同一条提示。这个集合控制的是本次流内的提示去重,不能证明跨请求重试时工具只执行一次。

    收到 TOOL_EXECUTED 后,处理器解析请求参数,调用工具的 generateToolExecutedResult() 生成展示内容,并追加到聊天记录缓冲区。虽然内部消息包含实际 result,当前展示分支没有读取它,而是依据参数格式化内容。例如写文件工具会根据文件路径和内容生成代码块;工具方法即使返回“文件写入失败”,该展示分支也没有据此明确标成失败。处理器和写文件工具共同体现了这个区别。

    最终完成状态也有类似边界:Vue 的完成回调调用 buildProject(),但没有检查其布尔返回值,随后仍结束流;HTML 和多文件模式的解析保存异常也只记录日志。因此,当前 done 可以表示响应流正常走完,不能据此确认文件保存或构建一定成功。

    进一步完善时,应把工具实际返回结果中的成功或失败传给展示层,构建失败也要进入明确的失败状态,再决定是否发送业务完成事件。这样模型记忆、页面提示与工程产物才能依据同一份执行事实更新,而不是都依据一段“看起来成功”的文字。

  2. 多轮对话记忆如何分级管理,并设计缓存策略?

    展开收起

    核心回答

    平台为同一个应用的连续修改保留三类对象:MySQL 保存展示用对话历史,Redis 支撑模型使用的消息窗口,Caffeine 缓存应用的 AI 服务实例。窗口按 appId 定位,配置最多20条消息;服务实例按应用与生成类型复用,减少重复创建和历史装载。

    用户继续修改同一应用时,模型结合当前窗口与新要求生成或调整代码。服务实例需要重新创建时,再从数据库取最近历史回装窗口。因此,历史是可回看的记录,窗口是本轮模型上下文,实例缓存是运行对象的生命周期管理,不能把三者都叫作长期记忆。

    一次请求与历史回装怎样连接

    聊天入口先检查应用归属,将本轮用户消息写入数据库,再通过 AiCodeGeneratorServiceFactory 获取服务。工厂使用 appId + codeGenType 作为 Caffeine 键,缓存未命中时才创建实例。

    创建时构造 MessageWindowChatMemory:以 appId 为 ID,接入 RedisChatMemoryStore,设置 maxMessages(20)。这里是20条消息,不是20轮对话,也不是 Token 上限。随后调用 loadChatHistoryToMemory(),按应用查询最近20条数据库历史;查到记录后反转为时间正序,先清空当前窗口,再把用户、AI 展示消息分别转成 UserMessage、AiMessage 加入。缓存命中时不会每轮重做这段装载。服务工厂和历史回装方法展示了这个顺序。

    数据库回装恢复的是这些展示消息,不是先前完整的工具调用对象、调用 ID 和 ToolExecutionResultMessage 轨迹。对继续改代码的任务,历史可以说明用户之前提出过什么,当前项目文件仍是判断代码现状的依据;不能仅凭一段旧回答认定某个文件已经修改成功。

    缓存策略与连续修改的取舍

    Caffeine 配置最多1000个服务实例,并设置写入后30分钟、访问后10分钟的过期策略;它控制服务对象的复用,不负责提取用户偏好。Redis 消息存储另有 TTL 配置,数据库历史不会因为本地实例被淘汰而自动删除。

    例如用户先要求生成表单,再要求“提交时显示加载状态”,近期窗口能保留这次修改的背景;较早的具体实现已经移出窗口时,应按需读取当前文件或相关历史,而不是无限扩大窗口。消息数窗口简单直接,但单条代码很长时仍可能超出模型预算,后续可评估 Token 窗口或保留关键修改摘要。

    同一应用的请求会复用相应记忆和代码目录,实例缓存不等于请求互斥。若允许并发修改,需要另行设计应用级串行或版本检查;当前配置本身没有保证这些语义。

    参考资料

    • 国内:JavaGuide:Agent 记忆系统:参考工作记忆、跨会话事实与记忆读写生命周期,组织通用分级,并解释保存聊天记录不等于完成长期记忆提取。
    • 国内:zero2Agent:记忆与上下文:采用状态、模型上下文与持久记录的区分,解释回装历史和恢复完整执行轨迹是不同目标。
    • 国内:WeThinkIn:记忆与上下文工程:参考作用域、版本和派生副本的管理,说明缓存键与失效策略为何需要对应具体主体和数据。
    • 国内:阿里云百炼:长期记忆:借鉴从对话提取特定事实、按记忆主体召回的方式,说明长期偏好与普通消息窗口、对象缓存的区别。
    • 国外:LangChain4j:Chat Memory:依据 Memory 与 History、消息窗口与存储接口的区别,解释模型可见消息和完整展示历史,以及消息数限制的含义。
    • 国外:Caffeine:Eviction:依据容量、写入过期和访问过期策略,解释服务实例缓存的生命周期与窗口、历史保留策略为何分开。
  3. 如何约束并验证 AI 生成代码的质量?

    展开收起

    核心回答

    这个平台要把生成完成、构建成功、部署完成和需求满足分开判断。实际单体 App 链路中,模型流结束后可以发出 SSE done,但这不能证明文件、构建和业务功能都正确;Vue 应用的正式部署入口会重新构建,检查返回值和 dist,通过后才复制产物并更新部署信息。

    构建能发现依赖、语法和打包问题,不能证明页面已满足用户需求。进一步完善时,应从需求提取可执行的页面验收条件,将构建和功能验证分别记录,失败后再反馈具体证据修正;这些验收能力需要另外接入。

    为什么流结束不能当作质量通过

    AppController 在内容流正常完成后追加 done 事件,它表达的是这次响应结束。Vue 生成分支在模型完成回调中调用 VueProjectBuilder.buildProject(),但没有检查其布尔返回值,随后便完成流。因此,即使构建器返回 false,前端仍可能收到正常结束事件。

    HTML/MULTI_FILE 分支则在收集完整文本后解析、保存代码;保存异常目前只记录日志,也可能不改变流的正常结束状态。生成片段已经展示给用户,与文件已经成功保存,是两个不同结果。生成门面和响应结束事件可以对照这两层状态。

    正式部署用哪些实际条件放行

    deployApp() 先检查应用及归属、定位生成目录;Vue 类型调用构建器,成功后才使用构建产物发布:

    1. VueProjectBuilder 检查项目目录和 package.json,依次执行 npm install、npm run build。命令超时、异常或非零退出码都会返回失败;最后检查 dist 存在且是目录。
    2. 部署入口检查 buildSuccess,为 false 就抛出构建失败;随后再次检查 dist 是否存在,将复制来源切换为该目录。
    3. 把产物复制到部署目录,更新 deployKey 和部署时间,再返回访问地址。HTML/MULTI_FILE 类型按已有静态文件部署,不经过 Vue 构建分支。

    构建器判断命令与产物,部署服务决定失败是否阻断发布。相同的构建函数,在调用方忽略返回值与据此拒绝发布时,形成的交付保证不同。

    这里还有产物与记录的一致性边界:构建器没有显式清理旧 dist 或校验产物的生成版本;复制文件与更新部署记录又是分步操作,记录更新失败不会自动撤回已复制的文件。进一步完善时,可以为构建产物增加版本标识,让发布目录切换与部署记录指向同一版本;不能只凭目录存在就确认这次发布内容正确。

    模型质检和需求验收放在哪里

    仓库另外提供 LangGraph4j 工作流实验,将生成、模型质检和项目构建组织成节点;它没有接入上述 App 聊天主路径。实验中的质检节点在调用异常时设置为通过,构建节点异常时会返回原代码目录,因此不能把节点名称当成主应用已有的质量门禁。

    如果后续把模型质检接入正式链路,可以用它发现可能的问题,但仍由真实构建和独立用例确认结果。例如需求要求表单有姓名、邮箱校验及提交后的反馈,即使 npm run build 成功,缺少邮箱校验仍应判为需求未完成。可按这些条件操作页面、检查实际结果,并把失败步骤、预期和实际表现交给生成环节修正,再重跑失败用例及相关回归。

    这类需求验收是进一步设计。发布判断应读取明确的构建和验收结果,避免用“模型已停止”“返回了一个目录”或“另一个模型说通过”替代它们。

    参考资料