
先说清楚一件事我最近把一款智能体对话 App 从零到一完整做了一遍从产品定义、技术选型、服务端架构到客户端流式渲染、会话历史管理、配置修复踩了不少坑也总结了一套可以直接照搬的打法。这篇文章不聊虚的就把整个项目的设计思路、核心实现和调试过程掰开揉碎讲清楚适合正在做或准备做类似 AI 对话产品的开发者、独立开发者以及想用开源智能体框架快速搭业务的人。1. 做之前先想明白智能体对话 App 到底解决什么问题1.1 先别急着写代码把产品边界划清楚很多人一听智能体对话 App第一反应就是接个大模型 API套个聊天界面完事。真这么干做出来的东西跟网页版聊天工具没有任何区别用户凭什么装你的 App我在这款 App 里一开始就定了三个原则第一对话必须是任务导向的不是纯闲聊第二智能体要有可配置的人设和工具调用能力不同场景能挂不同知识库第三会话历史要可靠用户关了 App 再打开聊天记录不能丢。这三个原则直接把产品和普通聊天壳子区分开了。智能体对话的产品定位不是另一个 ChatGPT 客户端而是能帮你干具体活的对话助手。比如用户可以在 App 里创建翻译智能体周报智能体客服智能体每个智能体有自己的 system prompt、知识库和可用工具这种配置化的设计才是智能体产品和单纯聊天的本质区别。1.2 目标用户和场景决定了技术选型我做的这款 App 面向两类用户一类是 C 端普通用户他们不需要懂提示词工程打开就能用另一类是小的团队或企业用户需要一个能配置角色、上传文档、对内对外的对话入口。这个定位直接影响了技术选型C 端用户在意的是启动速度、消息推送、离线访问所以客户端不能纯 webview要有原生壳子。B 端用户在意的是数据归属、会话留存、权限管理所以服务端要做多租户隔离会话数据必须落库。因为涉及文档上传和知识库检索光靠大模型的上下文窗口不够还得引入向量检索RAG。说白了产品形态决定了这不是一个小 demo而是一个前后端完整的工程。我把整个系统拆成了四层客户端App、接入层API Gateway、智能体编排层Agent Orchestrator、模型与检索层LLM Vector DB每一层单独部署、单独扩展。2. 智能体框架选型自研编排还是用现成平台2.1 Dify 这类平台能帮你省掉哪些事做智能体对话绕不开的一个问题是Agent 编排逻辑自己写还是用 Dify、Coze 这类现成平台我在项目前期特意用 Dify 搭了一版原型体验非常快。Dify 这类平台把 prompt 编排、知识库上传、工具插件、会话历史管理都做成可视化操作你只需要在界面上定义一个客服智能体挂上文档配置好模型API 一调就能出一个能用的对话接口。对于快速验证产品、给客户做 POC这套东西效率确实高。但它的问题在后期暴露得很明显定制化受限。我需要一些特殊的对话策略比如多个智能体之间的路由、按用户等级动态切换模型在可视化编排里做起来很别扭。数据链路黑盒。平台帮你处理了对话历史、检索、模型调用但出了问题你很难定位是哪一环出了问题。我调试的时候遇到一个客服连续对话中断的问题翻了半天日志才发现是平台侧的会话 ID 策略和我客户端对不上。成本不可控。平台托管虽然省了运维但按调用量计费当用户量上来之后单次对话的边际成本比自研高不少。2.2 自研 Agent 编排的核心结构权衡之后我选择了自研编排层但保留了 Dify 的很多设计思路。一个最简的 Agent 编排核心就三个组件会话状态机负责维护当前对话的状态包括当前智能体 ID、上下文消息列表、等待中的工具调用、本轮对话的 token 消耗。工具注册表每个工具就是一个可调用的函数有名字、描述、参数 schema。智能体通过模型决定是否调用工具、调用哪个工具。上下文组装器把系统提示词、历史消息、检索到的知识片段、工具调用结果按顺序组装成最终发给模型的 messages。我一开始犯过一个错把工具调用结果直接塞进历史消息里结果上下文越来越长第二轮对话就开始失忆。后来改为上下文组装器统一管理——只保留最近 N 轮的关键信息超出窗口的部分做摘要压缩。这个设计直接解决了客服连续对话场景下最常见的上下文断裂问题。2.3 模型选择与切换策略智能体对话 App 的模型层不能绑死一家。我做了模型网关兼容 OpenAI 格式的接口底层可以切换不同的模型服务。切换时只需要改配置不用改代码。这引出一个关键的工程问题模型能力分层。我把对话场景分成三层轻量闲聊和意图识别用小模型响应快、成本低主对话生成用中大型模型保证回答质量复杂推理和工具调用用强推理模型必要时走更长的思考链。在网关层根据对话的复杂度动态路由实测下来单次对话成本能降 40% 左右而且用户体感上响应更快。这里面有个小技巧判断复杂度不能全指望模型可以在客户端上做一个加强模式的开关用户手动选择是否启用更贵的模型同时也用关键词和意图分类做个兜底判断。3. 会话管理的工程化落地历史记录、存储与配置3.1 对话历史的存储架构做智能体对话 App最容易被低估的就是会话管理。很多开发者一开始就把聊天记录写在内存里或者随便存个本地文件等用户量上来或者用户换了设备问题就全出来了。我在设计会话存储时用了分层方案客户端本地层用 SQLite 存最近 50 条消息保证 App 冷启动后秒开聊天界面不需要每次等网络请求。这层的核心价值是离线可用和流畅体验。服务端持久层用 PostgreSQL 存全量会话记录每条消息包含会话 ID、角色、内容、时间戳、token 消耗、关联的智能体 ID。服务端保存的是唯一可信数据源客户端本地数据只做缓存。缓存层用 Redis 存最近活跃会话的消息列表避免每次请求都查数据库。Redis 的 key 设计成session:{sessionId}:messages设置 TTL 为 24 小时过期后自动从 Postgres 回源。这套三层结构解决了一个经典问题对话记录跟本地还是跟账号。我的做法是两者兼顾——本地保证离线体验云端保证跨设备同步。用户登录后客户端拉取云端会话列表与本地做合并冲突策略是以服务端最新修改时间为准。3.2 多轮对话的上下文组装策略多轮对话不能简单地把历史消息全量塞给模型原因有两个一是窗口长度有限二是无关历史会干扰模型输出。我用的策略是滑动窗口 摘要记忆保留最近 10 轮20 条消息的原始消息超过 10 轮的部分用一次轻量模型调用生成一段摘要放进系统提示词里如果中间涉及知识库检索会把检索结果作为独立消息插入并且标注来源方便模型引用时给出来源信息。这个策略的好处是长对话依然能保持上下文连贯同时不会把上下文窗口撑爆。测试时我跑过一个 50 轮的连续对话模型依然能准确记住用户最开始提到的需求这就是摘要记忆起的作用。这里有一个实操细节摘要生成不要每次都重新生成整个对话的摘要而是增量式——每次只对上一次摘要 最近新增的 10 轮做压缩。这样一次长对话下来摘要生成的调用次数不会随轮数线性增长。3.3 配置文件设计别把配置写死在代码里config.toml 无法加载、配置损坏导致对话无法继续这类问题其实在我们的 App 里也会出现只是换了个形式。我的经验是所有可调整的参数都要走配置中心而且配置要能热更新。具体来说我用了本地 TOML 文件 远程配置中心用 etcd 或简单的 JSON 配置接口结合的方式本地 TOML 配置负责 App 启动时的基础参数比如 API 地址、超时时间、模型默认参数远程配置中心负责动态调整比如切换模型、调整温度参数、上架新智能体。配置文件必须做版本管理和内容校验。我踩过一个坑在测试环境改模型参数时不小心把配置项写成了不存在的模型名结果线上会话全部报错用户侧表现为对话串无法继续。后来加了一个配置校验器——加载配置时先做 schema 校验出现未知字段或非法值就直接拒绝加载并回滚到上一个可用版本。如果你的配置已经损坏最简单的排查方式是看启动日志里第一个报错的位置然后把配置项逐个二分注释掉缩小范围。我用这个办法修过一次诡异的配置问题最后发现是一个特殊字符的编码问题重新用 UTF-8 保存就好了。4. 客户端实现与前后端联调从界面到流式协议的完整链路4.1 客户端技术选型我用 uniapp 的考量客户端我最终选了 uniapp Vue3 的方案主要原因是需要同时覆盖 iOS 和 Android又不想维护两套原生代码。对于以对话为核心交互的产品uniapp 的页面开发效率够用复杂的原生能力比如推送、语音识别可以通过插件市场找到现成方案。在界面布局上我参考了主流对话 App 的模式底部是消息输入框 录音按钮可选顶部是当前智能体信息和会话管理入口中间消息列表用虚拟滚动保证消息多了不卡顿。但 uniapp 有个坑长列表渲染性能不如原生。我短信几百条没问题几千条就出现输入卡顿。后来做了两个优化第一虚拟列表只渲染可视区域的消息第二消息组件用v-once或自定义缓存避免反复渲染历史消息。实测下来 2000 条消息的会话也能保持流畅。4.2 流式对话协议SSE 还是 WebSocket智能体对话 App 的核心体验是字一个一个蹦出来也就是流式输出。选流式协议的时候我在 SSE 和 WebSocket 之间纠结了很久最终方案是两者都用普通对话消息用 SSE。服务端把大模型生成的 token 逐个推给客户端简单可靠天然支持断线重连配合 HTTP/2 没有连接数限制问题。需要双向实时交互的指令比如用户在对话中触发工具调用、需要 App 端执行某个操作用 WebSocket。流式输出的核心字段设计如下{ session_id: sess_20250101_abcdef, message_id: msg_1720000000_001, type: delta, content: 你好, finish_reason: null }结束时返回finish_reason: stop客户端拿到这个标记才把消息完整写入本地数据库和界面。这里有个细节不要在收到第一个 delta 时就覆盖掉用户上一轮的输入框内容要等到finish_reason到达后才完成整条消息的落库。SSE 的断线处理也要做。我的策略是客户端维护一个消息序号每次重连时把已接收的最大序号发给服务端服务端从断点继续推而不是从头开始。这样用户在弱网环境下不会看到消息重复或缺失。4.3 前后端联调的协议设计一次完整的对话请求一次完整的智能体对话请求我设计成这样的流程客户端组装请求POST /v1/chat{ agent_id: translation_agent, session_id: sess_20250101_abcdef, messages: [ {role: user, content: 帮我把这句话翻译成英文今天天气很好} ], stream: true }服务端先检查会话权限和智能体状态然后调用编排层组装上下文。编排层确定是否调用工具比如翻译智能体需要先查术语库如果需要会先返回一个tool_call事件客户端展示正在查询术语库...的中间状态。工具结果返回后编排层再次调用模型生成最终回答以 SSE 流式推给客户端。客户端完整渲染后自动上报消息状态POST /v1/messages/ack服务端标记该消息已送达。这个协议里最重要的设计是区分了type: text_delta、type: tool_call、type: tool_result、type: done四类事件。客户端根据不同类型渲染不同 UI文字增量直接显示工具调用显示一个动画提示工具结果可以在消息里折叠展示。这样用户能理解为什么 AI 回答之前停了一下。联调的时候最容易出问题的是事件顺序。我的建议是服务端在开发环境里记录一份完整的事件流日志每个会话一个文件前端出问题直接对日志能省大量排查时间。4.4 语音交互和离线兜底智能体对话 App 不能光有文字输入。我加了语音输入用系统自带的语音识别iOS 的 Speech 框架、Android 的 SpeechRecognizer识别结果直接填充到输入框。这个方案比接入第三方语音识别省事很多而且对中文的识别准确率够用。还有一个不能忽略的点弱网和离线状态。用户在电梯、地下车库这种场景打开 App如果直接转圈等超时体验很糟糕。我的方案是请求发出后 3 秒内没收到响应立刻提示网络不畅消息将在恢复后发送同时把消息写入发送队列队列里的消息通过网络状态监听器uniapp 里的uni.onNetworkStatusChange在恢复网络后自动补发补发成功后对比服务端返回的 session_id 与本地消息的 session_id如果对不上说明会话被重置提示用户刷新。这套离线兜底机制做完之后App 在弱网环境下的崩溃率和用户投诉明显下降。5. 测试与上线从模拟调试到真机发布的完整路径5.1 用指令模拟器和大量回归用例验证对话智能体对话 App 的测试和普通 App 不太一样它高度依赖大模型的不确定性。同一个问题模型每次回答可能都不一样这就导致自动化测试不能只断言回答内容精确匹配。我维护了一套对话回归测试集按场景分三层功能层验证工具调用、知识库检索、会话历史这些确定性的功能是否正常。比如当用户请求查询订单状态时是否正确触发订单查询工具。内容层验证回答是否包含关键信息点用关键词匹配 语义相似度做双重判定不要求逐字一致。行为层验证对话过程中是否有严重问题比如是否出现幻觉、是否答非所问、是否在用户明确表达不满时没有道歉。这套测试集每次发布前跑一遍基本上能拦截掉大部分体验级 bug。跑回归的时候我是把大模型的温度参数调成 0尽量减少随机性对测试的干扰。初次开发完我还做了双人对话的特殊测试让两个智能体互相聊天 20 轮观察上下文是否会出现自我矛盾。这个方法能很快暴露上下文组装器的 bug。5.2 遇到的问题抓包失败、推送收不到、权限申请被拒上线前测试阶段我碰到了几个典型问题值得单独拿出来说说。第一个是抓包失败。调试客户端请求时我用 Charles 和 whistle 抓包结果发现 iOS 上很多请求根本抓不到。原因是 App 如果没有配置允许 HTTP 抓包iOS 的 ATSApp Transport Security会拦截明文请求。解决方法是在开发环境配置NSAllowsArbitraryLoads为 true但上线前必须删掉。Android 抓包失败多数是因为目标包默认不走系统代理需要借助测试工具配置代理或者用 root 设备装证书。第二个是消息推送延迟。用户不在线的时候智能体完成了一次长时间的工具调用要把结果推送到用户手机。我刚开始用普通的 APNs/FCM 推送结果经常延迟 30 秒以上。后来改成一个两步方案服务端先把消息写入数据库并生成推送通知用户点开通知后直接拉取消息详情不依赖推送内容本身。这样推送即使慢一点用户体验也不会受太大影响。第三个是 Android 权限申请被拒。语音输入需要麦克风权限录音需要存储权限如果一上来就把所有权限弹窗堆给用户很容易被用户拒绝。我的做法是细分权限场景首次进入对话页才申请麦克风权限在用户点击语音按钮时才申请存储权限并且每次申请都配上解释文案说明用途。实测权限通过率提升了一倍。5.3 从开发版到正式发布签名、版本管理和灰度App 发布的流程看着简单但每一步都有坑。iOS 需要开发者账号 证书 描述文件第一次配置时很容易搞混 Development 和 Distribution 证书。我建议建一个表格维护每台设备的 UDID 和每个描述文件的到期时间。Android 发布要到应用商店走审核特别要注意的是隐私政策的链接地址以及智能体对话这类功能涉及的敏感权限声明。版本管理我用的是语义化版本号每次发版都配套一个 changelog。为了控制风险我做了灰度发布先放 10% 的用户观察崩溃率和会话失败率没有异常再逐步放量到 50%、100%。灰度发布一定要配监控。我的服务端日志面板里放了几个关键指标SSE 断连率、平均首字延迟、消息落库失败率、工具调用成功率。任何一个指标超过阈值立刻暂停灰度并回滚。上线后我实际遇到过一次断连率从 0.5% 飙升到 15% 的情况排查发现是消息队列的消费者线程池被某个阻塞工具调用占满了这也是在灰度阶段发现的避免了大规模线上事故。6. 上线后的迭代方向销售智能体、私密对话与持续优化6.1 从通用对话到垂直场景销售智能体案例App 上线跑通之后我开始往垂直场景深耕最典型的是销售智能体。销售场景的对话和通用闲聊差别特别大。销售智能体需要记住客户的历史沟通记录、识别客户意向等级、在合适的时机发送产品资料/报价单还要能对接 CRM 系统。我做的销售智能体实现方式是在编排层增加了一个客户画像组件在每次对话前先把客户信息姓名、公司、历史订单、上次沟通摘要从 CRM 拉取出来合成到系统提示词里。同时定义了一个高意向识别器模型在对话中识别到用户有明确购买意向时调用 CRM 写接口自动更新客户状态。落地效果非常明显销售团队反馈以前一个销售一天能给 30 个客户发消息用了智能体之后客户的首次响应率提高了一倍而且销售不需要手动记录沟通摘要系统自动生成第二天接着聊也接得上。6.2 私密对话与数据隐私策略智能体对话 App 绕不开隐私问题。很多用户会问我的聊天记录是不是在被你们用来训练模型我的做法是三个层面数据分层存储会话数据分为默认存储和隐私存储两类。隐私会话默认不参与任何分析不进入训练数据仅用于该用户的会话恢复。用户可控删除用户可以在设置里一键删除全部会话数据服务端 72 小时内物理清除并提供删除确认回执。模型服务隔离调用模型服务时对隐私会话使用零留存的部署方式模型服务商不保留任何输入输出。当然这里也要说清楚任何 AI 对话产品真正保护用户隐私的核心是从产品层面就不要去收集和记录不必要的敏感信息。不要抱着先存下来以后有用的心态去囤数据。6.3 性能优化和成本控制的持续迭代App 上线后性能和成本是持续要盯的两件事。性能方面我把 App 冷启动时间从 2.8 秒优化到了 1.2 秒主要做了三件事第一首页的会话列表改为本地优先渲染先展示 SQLite 里的数据再静默同步云端第二模型配置和智能体列表加了本地缓存不用每次启动都请求网络第三PNG 图标全部换成 WebP减少包体和资源加载时间。成本方面大模型 API 是最大支出。我做了模型降级策略当用户使用的是基础功能时默认走性价比更高的模型当检测到对话涉及复杂推理或用户主动触发深度思考模式时才切换到大模型。这个策略让我的 API 成本下降了约 35%。我实际的体会是智能体对话这个赛道产品层面的差异化才是核心基础技术再炫用户体验跟不上照样留不住人。做这个 App 最大的收获不是掌握了大模型 API 的调用方式而是真正理解了对话即产品这句话——工程上把每一环做扎实产品才能站得住。最后再分享一个小经验如果你也打算做这类 App一定要从第一天就把会话历史管理和配置管理做好。这两个基础模块早期怎么看都不起眼等用户量起来之后它们决定了你的 App 是能稳定增长还是天天在处理对话记录无故丢失配置错误导致服务不可用的工单。先把这个底盘打牢再往上加智能体的花活这条路我替你试过了走得通。