最近在做AI对话App的项目原本以为所有工作量都会集中在模型调优和对话体验上真正动手才发现很多人第一步就卡在了“项目创建和运行”这里。倒不是说这一关有多难而是从空目录到服务能本地跑起来的整个流程里藏着大量“默认你会但新手根本不知道”的细节技术栈怎么选、IDEA 2024怎么建Web项目、前端怎么用pnpm初始化、后端模型API怎么接、遇到报错怎么定位……这篇文章就把我这次从零开始搭建并跑通AI对话App的完整过程写下来适合正在准备做AI应用开发、想搞懂项目创建到运行全流程的人参考。1. AI对话App的骨架设计先定形态再选技术栈1.1 你的App到底属于哪一类做AI对话App之前先别急着打开IDE而是要回答一个问题你做的“App”是给用户在浏览器里用的Web应用是打包成安卓/iOS的手机客户端还是偏向Agent自动化任务的工具型应用这个问题的答案直接决定了项目创建方式。我这次的目标是一个功能完整的对话应用用户输入问题后端调用大模型接口模型流式返回内容前端实时展示。最常见、也最适合快速验证的形态就是前后端分离的Web应用——浏览器即App开发调试成本最低后续要转成手机App也可以用同一套后端接口。技术栈选型上后端我用了Java 17 Spring Boot 3.x前端用Vue 3 Vite pnpm。没有用更花哨的方案原因后面逐一说明。1.2 技术栈选型的真实理由后端选择Spring Boot 3最重要的原因是生态成熟。Spring Boot自带内置Tomcat项目创建完成后不需要额外配置外部容器直接Run就能起服务这对“让项目先跑起来”这个目标非常友好。IDEA 2024对Spring Boot的支持已经很完善创建Web项目、运行、断点调试都是点按操作不需要自己手搓一堆配置文件。Java本身的类型安全也让大模型接口返回的数据结构处理更可控——那些字段是String、那些是List、哪些可能为空编译期就能发现一堆低级错误。前端选Vue 3 Vite是因为它足够轻。Vite的冷启动速度非常快改完代码页面秒级刷新开发体验比老一代构建工具好太多。pnpm则解决了node_modules体积大、依赖安装慢的问题同一个项目用npm要装一分钟用pnpm可能十几秒就完事而且磁盘占用只有npm一半不到。移动端后续如果要接Flutter也不影响——Flutter项目可以直接复用这个后端接口只是把UI层换成Dart代码。提示技术栈没有绝对的对错核心是“能让你最快跑起来并验证想法”。团队熟悉哪套用哪套不要为了追新而选择一个没人会的框架。1.3 运行环境清单项目能跑起来依赖以下运行环境建议提前确认环境版本要求用途JDK17及以上Spring Boot 3.x要求Java 17起步Maven3.6及以上后端依赖管理Node.js18及以上前端构建工具链pnpm8及以上前端依赖安装Redis5.0及以上会话上下文缓存可选但推荐大模型API Key任意兼容OpenAI协议的服务对话能力的来源这里特别说明一下Redis如果只是本地开发、单用户调试可以先用内存Map存会话不引入Redis。但如果目标是做出一个支持多会话的AI对话App建议从一开始就引入Redis。因为大模型对话需要携带上下文也就是把历史消息一起传给模型这些消息存在哪里、怎么和用户Session关联用Redis是教科书级的标准答案。2. 项目创建的真实操作IDEA 2024 Vite pnpm一个都不能少2.1 用IDEA 2024创建Spring Boot后端项目打开IDEA 2024选择新建项目。这里要注意新版IDEA的创建向导和旧版有区别项目类型要选Spring Boot而不是传统的Java Web项目模板。Java版本选17构建工具选MavenSpring Boot版本选3.2.x这类稳定版本。依赖项勾选Spring Web、Spring Validation、Spring Data Redis、Lombok。创建完成后IDEA会自动生成标准的Maven项目结构src/main/java、src/main/resources、pom.xml。第一个坑往往出现在这里——Maven依赖下载。由于网络原因maven-central仓库下载可能非常慢甚至失败。解决方案是在Maven的settings.xml中配置阿里云镜像之后pom.xml里的依赖基本秒下。第二个坑是IDEA本身没有自动识别Maven项目导致右侧Maven面板为空。遇到这种情况在pom.xml上右键选择“Add as Maven Project”即可。对于IDEA运行Java Web项目的配置Spring Boot时代已经不需要手动配置外部Tomcat了。spring-boot-starter-web依赖自带内嵌Tomcat直接运行src/main/java下的SpringBootApplication主类内置容器就会启动在8080端口。如果是老的Servlet项目才需要配置Artifacts和外部Tomcat这一点在新手群里经常被混淆。2.2 用Vite和pnpm初始化前端项目前端项目创建有两种方式一种是用IDEA自带的前端项目向导另一种是用命令行。我习惯用命令行因为更通用换到任何环境都能复现pnpm create vite ai-chat-web -- --template vue执行后会生成一个Vite Vue 3的项目骨架。进入目录安装依赖cd ai-chat-web pnpm install这里大概率会遇到热词里那条经典报错——“pnpm : 无法将“pnpm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。原因很简单pnpm是用npm全局安装的但安装目录没有加入系统PATH。排查链路是这样的先确认npm install -g pnpm是否成功再执行npm config get prefix查看全局安装路径最后把该路径加入系统环境变量Path重新打开终端pnpm -v就正常了。前端项目还要装几个必要的库pnpm add axios element-plusaxios用来发HTTP请求element-plus提供对话列表、输入框、消息气泡等UI组件省去手写组件的成本。UI组件的选择不会影响项目能不能跑但能让你把更多精力放在业务逻辑而非样式上。2.3 初始化配置application.yml和.env应该写什么后端配置文件在src/main/resources/application.yml。最基本的配置如下server: port: 8080 spring: application: name: ai-chat-server data: redis: host: localhost port: 6379 ai: model: api-key: ${AI_API_KEY} base-url: https://api.example.com/v1 chat-model: deepseek-chat max-tokens: 2048 temperature: 0.7api-key用环境变量的方式注入而不是把真实Key写死在配置文件里。这个习惯很重要因为项目一旦提交到Git仓库然后推到公开平台写死在代码里的Key就等于泄露了。base-url是模型API的服务地址兼容OpenAI协议的服务基本都长这样只是域名不同。前端环境变量在项目根目录创建.env文件VITE_API_BASE_URLhttp://localhost:8080前后端分离开发时前端页面跑在5173端口后端接口跑在8080端口浏览器访问前端页面发请求到后端就存在跨域问题。所以开发阶段更推荐的做法是在Vite配置里加代理让前端请求/api开头的路径时自动转发到后端这样在代码里只需要写相对路径。Vite的vite.config.js配置如下import { defineConfig } from vite; import vue from vitejs/plugin-vue; export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });这套配置意味着前端页面发起fetch(/api/chat/stream)时实际请求会被Vite开发服务器转发到http://localhost:8080/api/chat/stream浏览器端不存在跨域问题后端也不需要额外开启CORS。2.4 建立一次“无AI版”的联调在接入模型API之前先让前后端能通过一次最简单的接口调用联通。后端写一个健康检查接口RestController RequestMapping(/api/ping) public class PingController { GetMapping public MapString, String ping() { return Map.of(message, pong); } }前端在页面里调用一次const res await fetch(/api/ping); const data await res.json(); console.log(data.message); // pong这一步验证的是整个链路前端端口、代理转发、后端启动、端口监听、响应序列化。如果这里通了说明项目创建和基础运行没有任何问题接下来的AI对话接入就纯粹是业务逻辑了。我见过太多人一上来就直接写对话接口结果前端怎么调都不通排查半天发现是代理没配上浪费时间。3. 接入模型API是核心从HTTP请求到SSE流式的完整链路3.1 为什么走HTTP协议而不是官方SDK很多模型服务商都提供了官方SDK比如用Python的openai库一行代码就能调用模型。但我的建议是如果后端是Java直接走HTTP协议反而更可控。原因有三个第一减少项目依赖。SDK本质上是把HTTP请求封装了一层引入一个SDK就是引入一堆传递依赖一旦SDK版本更新引起冲突排查成本远高于自己写一个WebClient方法。第二Java生态下官方对大模型接口的封装普遍一般错误信息经过SDK透传后往往丢失原始响应体出了问题很难定位。第三直接走HTTP意味着可以方便地切换任何兼容OpenAI协议的模型服务只需要改配置里的base-url和model名称代码一行不用动。3.2 后端实现流式对话WebClient FluxAI对话最核心的体验是“打字机效果”也就是模型生成内容边生成边显示。实现方式不是普通的一次性HTTP请求而是长连接流式传输技术术语叫SSEServer-Sent Events服务器推送事件。SSE协议非常简单服务端按行返回数据每行格式是data: 内容以空行分隔不同消息。Java后端用Spring WebFlux里的WebClient发起流式请求代码结构如下Service public class ChatServiceImpl implements ChatService { private final WebClient webClient; private final ModelProperties properties; public ChatServiceImpl(ModelProperties properties) { this.properties properties; this.webClient WebClient.builder() .baseUrl(properties.getBaseUrl()) .defaultHeader(HttpHeaders.AUTHORIZATION, Bearer properties.getApiKey()) .build(); } public FluxServerSentEventString stream(String sessionId, ListMessage messages) { return webClient.post() .uri(/chat/completions) .bodyValue(Map.of( model, properties.getChatModel(), messages, messages, stream, true, max_tokens, properties.getMaxTokens(), temperature, properties.getTemperature() )) .retrieve() .bodyToFlux(String.class) .mapNotNull(this::parseContent); } }这里有几个关键决策要解释。stream: true告诉模型服务端返回SSE格式的流。bodyToFlux(String.class)表示响应体按字符串行读取。每个String对象包含一行SSE数据需要从中解析出data:前缀后面的JSON再从JSON的choices[0].delta.content字段取出本次增量的文本。mapNotNull负责过滤掉心跳包这类无内容的数据。我最初踩过的一个坑是超时配置。Spring WebClient默认的响应超时对于流式接口来说太短了模型思考时间长一点就会断开连接。解决方法是给WebClient设置读取超时比如60秒甚至更长HttpClient httpClient HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) .doOnConnected(conn - conn.addHandlerLast(new ReadTimeoutHandler(120))); SslContext sslContext SslContextBuilder.forClient().build(); WebClient webClient WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient, sslContext)) .build();3.3 前端解析流式返回ReadableStream逐字读取前端拿到SSE流之后不能用普通的response.json()去解析而是要读取response.body也就是ReadableStream。核心代码如下const response await fetch(/api/chat/stream, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ sessionId, messages }) }); const reader response.body.getReader(); const decoder new TextDecoder(utf-8); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop() || ; for (const line of lines) { if (line.startsWith(data: )) { const payload line.slice(6); if (payload [DONE]) continue; const json JSON.parse(payload); const content json.choices[0]?.delta?.content || ; if (content) { appendMessage(content); // 追加到当前回答气泡 } } } }buffer的作用非常关键。网络传输是按字节包过来的一次reader.read()返回的数据不一定是完整的一行可能半行也可能多行。所以要把读到的字节先追加到buffer里再按换行符切分最后一行没有换行符的字节留在buffer里等待下一次读取合并。这就是流式解析的经典处理方式我一开始没写buffer结果模型输出的内容经常被截断成乱码排查了很久才定位到是分帧拼接的问题。为什么选SSE而不是WebSocket这是很多人的疑问。AI对话本质上是从服务端到客户端单向的流式传输用户发消息是一次独立的POST请求服务端回传是一路流。这种“一上多下”的模型用SSE天然合适实现简单、自动支持HTTP重连、穿透代理容易。WebSocket是双向全双工功能更强但如果只是为了接收模型输出用它属于杀鸡用牛刀还多引入一套连接管理的复杂度。3.4 上下文管理让每轮对话“记得住”模型本身不记忆任何历史对话所谓上下文就是每次请求都带上之前的消息记录。一个标准的消息结构是这样的[ {role: system, content: 你是一个友好的AI助手}, {role: user, content: 什么是AI}, {role: assistant, content: AI是人工智能的缩写……}, {role: user, content: 它有什么用} ]后端收到新对话请求时根据sessionId从Redis取出历史消息数组拼装上新消息一起发给模型。模型返回的content再追加到历史消息中存回Redis。这样每一轮对话都携带全部历史模型就能“记得”之前聊过什么。token长度的问题需要提前预防。对话越长历史消息越多超出模型的上下文窗口就会报错。我的处理方式是设定一个最大历史长度比如只保留最近10轮对话超过时丢弃最旧的消息。这个策略在AI对话App开发里非常常见属于在“记忆深度”和“token成本”之间寻找平衡点ListMessage trimmed new ArrayList(history); while (trimmed.size() MAX_MESSAGES) { trimmed.remove(1); // 保留第一条system消息移除旧的user/assistant消息 }4. 让项目跑起来的细节启动顺序、代理转发与第一次对话实测4.1 后端启动步骤与配置检查后端启动步骤顺序很重要。先启动Redis如果本地装了再启动Spring Boot主类。如果Redis没启动Spring Data Redis的配置会自动重试连接项目启动会拖时间甚至直接失败。Redis是本地开发环境最容易忽略的外部依赖建议用Docker一条命令起docker run -d -p 6379:6379 --name redis redis:7-alpine启动Spring Boot后看控制台日志。出现Started AiChatServerApplication表示启动成功Tomcat started on port 8080表示端口监听正常。如果端口被占用会报Port 8080 was already in use。处理方式有两种改端口或者找到占用进程杀掉。Windows下查看端口占用netstat -ano | findstr 8080 taskkill /PID 进程号 /F这里有个经验Java项目的报错信息一定要完整看完。很多人看到红色日志就慌其实关键信息往往在最后几行。比如APPLICATION FAILED TO START下面的Description和Action两段直接告诉你哪里配置错了、应该怎么改照着做就行。4.2 前端启动步骤与联调检查前端启动只需要一条命令pnpm devVite默认把服务跑在http://localhost:5173。打开浏览器按F12打开开发者工具切到Network面板重新刷新页面如果看到/api/ping请求返回200和{message:pong}说明代理配置成功。如果报错优先检查vite.config.js的proxy配置确认target端口和实际后端端口一致。一个非常容易踩的坑是后端端口改了但前端代理没同步改结果所有请求全部返回404。另一个坑是代理配置里的changeOrigin必须设为true否则后端拿到的Host头是前端地址某些鉴权逻辑会出问题。4.3 第一次完整对话实测前后端联通后把对话请求真正发出去。第一次实测建议不要画太多UI就在页面里写一个最简单的输入框和按钮点击后调用3.3节那段fetch代码。如果页面上正常看到“打字机效果”的文字逐字出现恭喜你整个AI对话App的核心链路已经完全跑通了。实测出现的拦路虎主要集中在三类第一类是401鉴权失败。控制台输出Invalid authentication credentials八成是环境变量AI_API_KEY没生效。IDEA里配置环境变量要在Run Configuration的Environment variables里填或者在启动前用命令行export AI_API_KEYxxxx设置。直接在application.yml里写死api-key: ${AI_API_KEY}但没设环境变量启动时会得到一个占位符字符串而不是真实Key。第二类是404。请求/api/chat/stream返回404先看Controller路径和请求路径是否完全一致再看produces MediaType.TEXT_EVENT_STREAM_VALUE有没有写对。还有一个隐蔽问题Spring Boot 3中如果Controller方法接收Flux返回值必须引入spring-boot-starter-webflux依赖否则不会按SSE处理。第三类是流式内容不展示。接口能通但内容是等全部生成完才一次性显示。这种问题几乎都是请求头里少了Accept: text/event-stream或者前端用了response.json()而不是读response.body。按下F12看响应类型如果Content-Type是application/json而不是text/event-stream就说明后端SSE没有生效。4.4 数据库和Key的安全配置一个成熟的项目从一开始就应该把敏感配置隔离。API Key不要出现在任何代码文件里统一走环境变量。如果是在IDEA里调试可以在.env文件或IDEA的环境变量配置中维护如果是在服务器上跑Linux的export或systemd的EnvironmentFile都能实现同样的效果。Redis如果部署在公网一定要设置密码不要用默认端口无鉴权裸奔否则扫描工具会直接连上去删库。5. 运行期高频报错的排查链路从命令行到浏览器逐个击破5.1 “无法将pnpm项识别为cmdlet”不只是环境变量这条报错在Windows环境非常典型但原因不止一个。最常见的情况是pnpm没经过npm全局安装。安装命令npm install -g pnpm如果npm -v本身都报错那是Node.js没装好需要去官网重新下载安装包。如果npm正常但pnpm命令找不到那么执行npm config get prefix把看到的路径一般是C:\Users\你的用户名\AppData\Roaming\npm加入系统PATH然后完全关闭并重新打开终端再执行pnpm -v。更隐蔽的情况是用PowerShell执行pnpm时报同一个错但CMD里执行却正常。这是因为PowerShell和CMD读取PATH的时机不同修改系统PATH后PowerShell需要以管理员身份重启才生效。原理是PowerShell的$env:Path会在会话启动时缓存一次旧会话里新加的路径不会自动刷新。解决办法就是新开终端这个坑不知道坑了多少人。5.2 “运行失败请查看提示信息”先分清是哪一类问题开发过程中经常看到“运行 core 失败请查看提示信息”这类模糊报错。我的排查思路是分层定位先把问题归到以下几类报错类型典型表现排查方向编译错误红字出现Compilation failure看具体报错的Class和行号依赖错误找不到jar包或模块检查Maven/Gradle仓库配置端口冲突Port already in usenetstat查端口并结束进程配置错误Failed to bind properties看是哪个配置项绑定失败运行时异常NullPointerException等看堆栈第一行的业务位置大部分“失败”提示后面都会跟具体信息关键是训练自己读完整日志的能力。Java报错堆栈从最底下往上读第一个出现的at com.xxx是你自己代码的位置上面那些at org.springframework基本都是框架内部调用不用深究。找到自己代码里的第一行问题基本就在那里。5.3 请求转发失败、CORS跨域、Key未配置的高频问题这三个问题是前后端分离项目里最容易挨个遇到的。CORS跨域的表现是浏览器控制台报Access to XMLHttpRequest at http://localhost:8080/... from origin http://localhost:5173 has been blocked by CORS policy。如果采用Vite代理前端请求都发到相对路径就不会触发跨域。如果确实需要前端直连后端地址比如后端部署在测试服务器上最省事的方式是后端加一个CORS配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, OPTIONS) .allowedHeaders(*); } }注意allowedOrigins不要写成*否则在需要携带cookie的后续开发中会踩坑。Key未配置的报错通常是401 Unauthorized或者Invalid API Key。我开发时习惯在启动时打印一段日志显示Key的前几位和长度不打印完整Key方便快速确认环境变量是否加载成功。这个做法在排查环境差异时特别有效。5.4 换机器、换平台后“程序无法运行”的真相热词里有一条很典型的报错“程序claude.exe无法运行: 指定的可执行文件不是此操作系统平台的有效应用程序”。这个现象解释起来很简单——下载了和当前操作系统不匹配的二进制文件比如在Windows 11上装了Linux版本的程序或者在64位系统上执行了32位程序。解决方式就是去官网重新下载对应平台的安装包没有别的捷径。这类错误在AI相关工具上特别常见因为这些工具的官方下载页通常同时提供Windows、macOS、Linux多个版本默认展示的可能是当前浏览器系统识别的版本但有人为了下载“推荐版”或“最新版”而手动选择选错了平台就会触发这条报错。凡是遇到“不是有效应用程序”的说法优先检查系统架构x86、x64、ARM和操作系统类型是否正确。6. 从能运行到能交付Agent能力与移动端扩展6.1 什么是Agent开发让对话App不只是聊天项目跑通了AI对话不再是“一问一答”的演示下一步就有两个方向可以走一个是把App做得更像一个Agent另一个是把Web App打包成真正的移动端应用。所谓Agent开发简单理解就是让模型不只输出文本还能调用工具完成实际操作。比如用户说“帮我查一下今天的天气”模型不直接回答而是识别出这是一个查询需求生成一个工具调用请求后端收到后调用天气API把结果返回给模型模型再用自然语言把天气情况表达出来。这个模式目前在行业里非常流行因为它让AI从“聊天机器人”变成了“能办事的助手”。最小实现路径是给模型定义一组JSON格式的函数列表模型根据用户输入决定是否调用某个函数、参数是什么。后端在收到函数调用请求后真正执行函数把执行结果作为一条新的role: tool消息追加到上下文中再让模型基于工具结果生成最终回复。这就是Agent开发最基础的协议链路。6.2 移动端方向用Flutter复用同一套后端如果你真的需要把App上架到手机应用商店不建议在Web端之外再写一套逻辑而是用同一个后端前端换成Flutter。热词里“如何用Android Studio创建Flutter项目”操作路径很清晰Android Studio里安装Flutter插件New Project选择Flutter填好包名用内置模拟器直接跑。也可以用命令行的方式flutter create ai_chat_app创建出的Flutter项目中lib/main.dart是入口用http包或dio包访问后端接口。由于是独立App而不是浏览器页面跨域概念不存在但需要注意网络安全配置Android 9及以上默认禁止明文HTTP请求Debug环境需要允许cleartextTraffic才能访问http://前缀的后端地址。这个坑很隐蔽项目能编译、能启动但请求后端一直失败最后发现是系统网络安全策略禁止了非HTTPS流量。6.3 本地模型部署的另一种选择如果不想依赖在线API想要完全本地化运行模型可以关注低显存运行模型的方案。核心思路是模型量化——把模型的权重精度从FP16降到INT4或INT8显存占用能降低百分之七八十同时在推理引擎层面做优化。目前比较成熟的方案是配合Ollama这类本地推理工具下载量化版本模型一行命令就能启动一个兼容OpenAI协议的本地服务此时base-url直接指向http://localhost:11434/v1后端代码完全不用改。本地模型部署需要注意两点一是模型参数量要和显卡显存匹配比如8GB显存跑7B量化模型已经比较吃力14B以上的模型至少需要16GB二是推理速度受CPU内存带宽影响较大没有独显的机器跑起来会很慢更适合做原型验证而不是生产环境。项目从创建到运行的全流程走通之后我再回看这次实操最大的体会是工程上的问题90%都不是“智商的差距”而是“经验的有无”。那些让人头疼的报错本质都是环境变量、依赖版本、端口配置、网络策略这几个固定环节的排列组合。把这套排查思路变成肌肉记忆再做任何AI应用项目创建和运行这一关基本不会再拦人了。最后补充一个小技巧每次项目跑通一个阶段比如后端启动成功、前端代理联通、SSE流式返回正常都值得把这时的依赖清单、配置文件、启动命令完整记录到项目的README里。这样下次换一台电脑或者隔几个月再看这个项目不用靠回忆照着文档十分钟就能重新跑起来。这个习惯帮我省下的时间远比我写文档花掉的时间要多得多。