简介:一套基于HTML的Coze智能体对话页面搭建方案,适合需要快速集成智能对话能力的前端开发者与API调用场景。方案覆盖完整Coze API调用流程,支持流式输出、图片直显、多轮对话记忆及Markdown解析,开发者只需替换COZE_API_TOKEN与COZE_BOT_ID两个参数,即可完成对接。压缩包共3个文件,以核心HTML页面为主,另含inscode配置与gitignore文件,整体仅7KB,轻量易部署。资源已有214人学习,内置SSE原生流式输出、响应式布局、错误处理与调试日志,能保障跨设备展示效果,也便于联调排错。对照源码可快速理解用户ID自动生成、图片链接自动解析与优化显示等实现细节,适合需要低成本验证智能体交互效果的个人开发者与前端工程师。
1. Coze智能体对话页面:为什么Bot做完了,还差一个能交付的页面
在Coze上把智能体的工作流、人设和知识库都调通之后,很多人会卡在一个地方:Bot在Coze的调试窗口里聊得好好的,客户和同事却没法用。你要么把链接丢给对方,让他注册一个Coze账号进预览页;要么截一堆图证明“这智能体真的能跑”。真正能交付的形态,是你把Coze智能体对话页面接到自己的网站、小程序或者企业微信里,让用户在一个没有Coze Logo的界面上完成对话。这篇文章讲的就是这个:如何从零搭一个可运行的Coze智能体对话页面,包括源码结构、鉴权方式、流式输出、工作流对接,以及让新手少走弯路的踩坑记录。
适合谁看?已经在Coze里建过Bot,想把它嵌入自有产品,但对前端和后端都不太熟的开发者。我会给出一个可以直接抄的本地运行方案,也把参数和边界讲清楚。
2. 三种对话页方案选型:官方组件、API自建、开源项目改
2.1 先分清三件事:Bot、对话API、对话页面
很多人把Coze上的Bot理解成一个“聊天机器人”,这没错,但落地页面时你要区分三个层级。
第一层是Bot本身。你在Coze里配置的人设、工作流、知识库、触发器,它运行在Coze的云端。第二层是对话API。Coze开放平台允许你通过HTTP接口把消息发给这个Bot,拿回回复。第三层是对话页面。这是真正呈现在用户面前的HTML/JS界面,它负责收集用户输入、调用API、把回复渲染成气泡。
有人误以为“把Bot发布了就等于有对话页”,其实Coze发布后给你的只是一个访问链接或渠道接入方式,不是任你改写的页面。有人反过来,以为“自己写对话页面就得重新做一个智能体”,其实你只要会用对话API,Bot的智能完全保留在云端。
这三层之间的关系是:对话页面只负责收发消息和展示,业务逻辑全在Bot里。这是整个落地方案的基本盘。
2.2 方案A:Coze自带发布渠道,最快但定制受限
Coze发布Bot时,可以直接生成一个网页链接,也能发布到飞书、微信客服等渠道。如果你只是做个内部演示,这个方案10分钟就能搞定,不用写一行代码。
但它的限制很明显:一是页面UI不可改,你没法替换Logo、配色、气泡样式,也没法在对话框上方加自己的产品介绍栏;二是如果你想把对话嵌到已有系统里,比如放在用户中心的订单页旁边,官方链接做不到这种嵌入。三是历史记录和用户体系在Coze侧,你拿不到用户在自己系统里的身份信息。
如果你的诉求只是“让同事试试我的Bot”,官方链接够了。本文后面所有内容,都是为“要把对话页变成自家产品一部分”的读者准备的。
2.3 方案B:API对接自建,本文采用的路线
Coze开放平台提供了对话API,这是自建对话页的核心。整体链路是:前端页面把用户输入发给你的后端,你的后端带上鉴权信息调用Coze的对话接口,再把回复以流式方式转发给前端。
这样做有三个好处。第一,鉴权信息不会暴露在浏览器端。Coze的API用Personal Access Token(简称PAT)鉴权,这类Token一旦出现在前端代码里,就等于公开了。新浪的“把这个Key换成你自己的”这种教程看着简单,实际是埋雷。第二,你可以在后端做用户身份映射、敏感词过滤、对话日志留存,这些在纯前端方案里很难补。第三,流式输出可以自己做控制,比如限速、缓存、重试。
代价是你需要维护一个极简后端。本文用Flask来做,因为它是Python开发者最熟悉、单文件就能跑的框架,新手友好。
2.4 方案C:开源对话项目二次开发
从GitHub上找一个现成的ChatGPT风格对话前端,比如chatbot-ui、lobe-chat这类项目,把API端点从OpenAI换成Coze,是另一种常见做法。
这类项目功能丰富,有流式渲染、会话管理、暗黑模式,省去很多前端工作。但问题在于:它们多数是面向OpenAI API协议设计的,Coze的API格式、鉴权头、消息结构都需要改。尤其是CORS策略,OpenAI允许的跨域配置和Coze开放平台不一致,你还得在后端做一层适配。
我的判断:如果只是做一个对话页,方案B的代码量不到300行,没必要引入一个几千文件的前端工程。如果你未来要做多智能体管理、知识库可视化、团队协作,那再考虑基于开源框架深改。
综上,本文的核心路线是:Flask做后端代理,原生HTML/JS做前端,Coze对话API做智能引擎。
3. 最小可运行源码:Flask后端代理与原生前端对话页
3.1 项目结构与依赖
先搭一个最精简的项目,目录如下:
coze-chat-page/ ├── app.py # Flask 后端,转发消息到 Coze API ├── requirements.txt # Python 依赖 ├── static/ │ └── index.html # 对话页面 └── config.py # 配置:Bot ID、API Key 等requirements.txt内容:
flask>=2.2 requests>=2.28为什么要分config.py?因为Bot ID和API Key这类信息散落在代码里,后面换环境时容易漏改。单独一个配置文件,部署时只动它就行。
3.2 Flask后端:鉴权、转发与流式响应
后端要做的事有且只有三件:接收前端POST过来的用户消息,带上Coze的PAT去调用对话接口,把Coze返回的流式内容转发给前端。核心代码:
# app.py import json import requests from flask import Flask, request, Response, render_template from config import COZE_API_KEY, COZE_BOT_ID app = Flask(__name__) COZE_API_URL = "https://api.coze.cn/v3/chat" def call_coze_chat(user_message, conversation_id=None): headers = { "Authorization": f"Bearer {COZE_API_KEY}", "Content-Type": "application/json", } payload = { "bot_id": COZE_BOT_ID, "user_id": "web_user_001", # 调用方自定义的用户标识 "stream": True, # 开启流式 "auto_save_history": True, # 让Coze自动保存对话历史 } if conversation_id: payload["conversation_id"] = conversation_id payload["messages"] = [{ "role": "user", "content": user_message, "content_type": "text", }] resp = requests.post(COZE_API_URL, headers=headers, json=payload, stream=True) return resp @app.route("/api/chat", methods=["POST"]) def chat(): data = request.get_json() user_message = data.get("message", "") conversation_id = data.get("conversation_id") upstream = call_coze_chat(user_message, conversation_id) if upstream.status_code != 200: return {"error": "coze api error", "detail": upstream.text}, 502 def generate(): for raw_line in upstream.iter_lines(decode_unicode=True): if not raw_line: continue yield f"data: {raw_line}\n\n" return Response(generate(), mimetype="text/event-stream") @app.route("/") def index(): return render_template("index.html") if __name__ == "__main__": app.run(host="0.0.0.0", port=9000, debug=True)逻辑说明:前端传进来的message和可选conversation_id被包装成Coze要求的消息结构,stream=True让Coze逐段返回,后端用生成器逐行转发。关键点是Authorization头放在后端,浏览器永远看不到这个Token。
参数说明:user_id是Coze侧用来标识对话方的字段,同一个user_id配合auto_save_history可以保持多轮上下文;如果你自己管理历史,可以把auto_save_history设为false,每次请求都把完整历史塞进messages。conversation_id是Coze返回的会话ID,第一次请求不传,Coze会生成一个新的,包含在响应里。
3.3 前端:流式输出、气泡渲染与加载状态
前端页面不需要任何框架,一个index.html加原生JavaScript就够了。核心逻辑是fetch拿到流式响应后,用ReadableStream解析SSE格式的数据。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>智能体对话</title> <style> body { max-width: 780px; margin: 40px auto; font-family: system-ui; } #chat-box { height: 480px; overflow-y: auto; border: 1px solid #ddd; padding: 16px; } .msg { margin: 8px 0; padding: 8px 12px; border-radius: 8px; } .user { background: #e3f2fd; text-align: right; } .bot { background: #f1f1f1; } #input-bar { display: flex; gap: 8px; margin-top: 12px; } #input { flex: 1; padding: 8px; } </style> </head> <body> <div id="chat-box"></div> <div id="input-bar"> <input id="input" placeholder="输入你的问题..." /> <button id="send-btn">发送</button> </div> <script> let conversationId = null; async function sendMessage(text) { const chatBox = document.getElementById('chat-box'); // 渲染用户气泡 chatBox.insertAdjacentHTML('beforeend', `<div class="msg user">${escapeHtml(text)}</div>`); // 创建空的机器人气泡,后面边读边填 const botMsg = document.createElement('div'); botMsg.className = 'msg bot'; botMsg.textContent = '正在输入...'; chatBox.appendChild(botMsg); chatBox.scrollTop = chatBox.scrollHeight; const resp = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text, conversation_id: conversationId }) }); if (!resp.ok) { botMsg.textContent = '请求失败: ' + resp.status; return; } const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let reply = ''; botMsg.textContent = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // SSE 消息以双换行分隔,按行逐个处理 const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { const trimmed = line.trim(); if (!trimmed.startsWith('data: ')) continue; let data; try { data = JSON.parse(trimmed.slice(6)); } catch { continue; } if (data.event === 'conversation.message.delta') { reply += data.content; botMsg.textContent = reply; chatBox.scrollTop = chatBox.scrollHeight; } if (data.event === 'conversation.message.completed') { // 从完成事件里拿会话ID const msg = data.message || {}; if (msg.conversation_id) conversationId = msg.conversation_id; } } } if (!reply) botMsg.textContent = '没有拿到回复,请检查后端日志'; } function escapeHtml(text) { const div = document.createElement('div'); div.appendChild(document.createTextNode(text)); return div.innerHTML; } document.getElementById('send-btn').onclick = () => { const input = document.getElementById('input'); const text = input.value.trim(); if (!text) return; input.value = ''; sendMessage(text); }; </script> </body> </html>逻辑说明:前端先把用户消息渲染出来,创建一个空的气泡,然后fetch请求后端接口。流式读取时,把SSE格式的data:行解出来,取conversation.message.delta事件的content字段追加到气泡文本上,实现打字机效果。当收到conversation.message.completed时,从消息体里取出conversation_id存到变量里,供下一轮请求使用。
escapeHtml是为了防止用户输入被当作HTML渲染,这一点在对话页里容易漏。Coze返回的Markdown格式没有做渲染,如果你需要,可以引入marked.js,但最小版本先把纯文本跑通。
3.4 本地跑通的最小命令
pip install -r requirements.txt python app.py然后浏览器打开http://localhost:9000。如果你看到能正常对话、文字是流式逐步出现的,说明整条链路已经通了。这一步通过后,再去接工作流和文件上传。
这里有一个新手常见的卡点:config.py里没有正确导入。检查config.py是否定义了COZE_API_KEY和COZE_BOT_ID,且与app.py在同一目录。空白或写错的Key,会让请求返回401。
4. 把对话页接到Coze工作流:文件上传、变量传递与话题隔离
4.1 Coze侧配置:发布Bot并拿到API Key
在Coze开放平台里,每个Bot都有一个对应的bot_id,在Bot的“发布”页面能看到。API Key则在个人令牌管理里生成,这一步注意:Key只显示一次,生成后立刻复制保存。
对话API调用的前提是Bot已发布到API渠道。很多人在这一步翻车:Bot只在开发环境调试过,没点发布,结果API调用返回“Bot不存在或未发布”。常见做法是在Coze的发布页面,选择发布到“API”渠道,得到一个可用于调用的版本。
发布后建议在Coze后台把“模型”和“工作流”分开测试:不带工作流的对话先跑通,再在工作流里加内容。
4.2 前端文件上传:把图片或文档传给智能体
Coze的对话消息里,content_type除了text,还支持image、file等类型。文件上传的做法是:先将文件传到Coze的文件接口,拿到file_id,再在对话消息里引用这个ID。
后端对应要加一个接口:
# app.py 中新增的上传代理接口 import os from flask import request COZE_UPLOAD_URL = "https://api.coze.cn/v1/files/upload" @app.route("/api/upload", methods=["POST"]) def upload_file(): upload_file = request.files.get("file") if not upload_file: return {"error": "no file"}, 400 files = {"file": (upload_file.filename, upload_file.stream, upload_file.mimetype)} headers = {"Authorization": f"Bearer {COZE_API_KEY}"} resp = requests.post(COZE_UPLOAD_URL, headers=headers, files=files) if resp.status_code != 200: return {"error": "upload failed", "detail": resp.text}, 502 return resp.json()前端把File对象包装成FormData发送:
async function uploadFile(file) { const formData = new FormData(); formData.append('file', file); const resp = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await resp.json(); return data.file_id; }拿到file_id后,下一轮对话请求的消息结构变成:
{ "role": "user", "content": "请分析这张图片里的表格", "content_type": "text", "file_ids": ["file_id_xxx"] }参数说明:file_ids是消息级别的字段,一次最多传10个文件ID。文件上传接口返回的file_id有效期有限,不要存太久,建议用户每次发送前重新上传。
4.3 多轮对话与话题隔离:conversation_id的正确用法
Coze的conversation_id是话题的隔离单位。同一个conversation_id之下的消息共享上下文;不同ID之间互相隔离。这对多用户场景很重要:如果多个用户共用一个conversation_id,他们的对话会互相串场。
更好的做法是:用户进入页面时,后端为每个用户生成一个UUID,后续所有请求都带上这个UUID作为user_id或conversation_id。不建议把Coze的conversation_id暴露给前端,因为它是平台层面的资源标识,你应该用自己系统里的会话ID去映射它。
我一般会在后端维护一个简单的映射表:用户会话ID -> Coze conversation_id。Coze首次请求返回的conversation_id存在后端,后续同一用户的请求自动带上。这样前端永远只传自己的ID,不关心Coze侧怎么存。
4.4 必调参数清单
下表是自建对话页时最常改的几个参数,按影响排序:
| 参数 | 位置 | 作用 | 建议值 |
|---|---|---|---|
stream | 请求体 | 是否流式返回 | true,用户体验差异巨大 |
auto_save_history | 请求体 | 是否由Coze存上下文 | 自己存历史就设false |
temperature | Bot配置/请求 | 回复随机性 | 客服场景 0.3,创意场景 0.8 |
max_tokens | Bot模型配置 | 回复长度上限 | 默认即可,太长影响首字延迟 |
user_id | 请求体 | 多用户隔离的依据 | 每个登录用户一个固定值 |
temperature在最开始可以不动,等对话质量有问题再调。Coze的模型参数在Bot编排页面也有一份配置,API请求里的参数会覆盖后台默认值,这个覆盖关系要记清楚,免得调了半天没效果。
5. 对话页面常见问题排查:五个必踩的坑
5.1 所有请求都返回401 Unauthorized
现象:前端页面能打开,但一发消息就提示401,后端日志显示Coze API返回未授权。
原因:COZE_API_KEY配错了,或者Key本身是临时的、已过期。另一个常见原因是把Key写到了前端,浏览器直接请求Coze API被CORS拦截,控制台看到的是CORS报错,不是401,但根因一样。
解决:先确认config.py里的Key从开放平台复制时有没有带空格或换行。然后用curl单独测一下:
curl -X POST 'https://api.coze.cn/v3/chat' \ -H 'Authorization: Bearer 你的KEY' \ -H 'Content-Type: application/json' \ -d '{"bot_id":"你的BOT_ID","user_id":"test","stream":false,"messages":[{"role":"user","content":"hi"}]}'如果curl能返回结果,说明Key和Bot没问题,问题在代码;如果curl也401,去Coze后台重新生成Key。
5.2 流式输出时前端页面卡住或文字一次性全部出现
现象:消息发出去后转圈很久,然后一次性蹦出一大段文字,没有打字机效果。
原因:前端对SSE的解析不对。常见的有两种:一是后端响应头没有mimetype="text/event-stream",浏览器把流式响应当作普通JSON一次性读完;二是前端解析时按\n\n分割,但Coze返回的行格式和预期不一致,导致done状态迟迟不来。
解决:先用Postman或curl看Coze原始返回格式,确认事件名是conversation.message.delta还是别的。不同版本的API事件名可能不同,按实际返回调整前端判断即可。如果你用的是requests的stream=True,记得在转发时不要对响应做.json()解析,否则会等全部内容到齐才返回,流式就失效了。
5.3 加了工作流之后,Bot完全不回复
现象:不带工作流时对话正常,接上工作流后API始终返回529或超时。
原因:工作流的执行时间超出了API的超时时间。Coze的API调用有时间限制,如果你的工作流里有慢节点(如网页搜索、大模型多次调用、文件处理),整体耗时就上去了。另一个原因是你把工作流的输入变量设成了必填,但对话消息里没有传对应字段,工作流直接报错。
解决:先在工作流调试页单测,看平均耗时是多少。超过30秒的工作流,不适合用同步方式接到对话页,常见做法是把耗时的部分拆成异步任务,或者把工作流改为“先快速回复,再让用户触发深度处理”的两段式设计。输入变量方面,在Coze的工作流里把变量默认值设上,不要留空必填项。
5.4 多轮对话回答不连贯,或前后矛盾
现象:用户问“它叫什么名字”,Bot回答“不知道你说的它指什么”;上上轮说过的内容,下一轮就忘了。
原因:大概率是你把auto_save_history设成了false,但没有在请求里带历史消息。Coze自己存上下文时,会自动拼接多轮;你自己接管后,就得保证messages里传入了完整历史。
解决:做历史管理时,最简单的做法是后端缓存在内存里,按user_id存消息列表,每次请求把最近10轮历史塞进messages。注意Coze对历史长度有限制,超出部分从最旧的开始裁剪。如果后端是多实例部署,内存缓存会失效,此时优先依赖conversation_id让Coze自己存。
5.5 页面能跑,但对话时浏览器控制台报 CORS 错误
现象:本地打开index.html时一切正常,部署到服务器后,浏览器报No 'Access-Control-Allow-Origin' header。
原因:我给出的前端页面是Flask的render_template渲染出来的,所以不存在跨域。但如果你把index.html单独放在Nginx里,而API请求却打到另一个端口或域名,就产生了跨域。
解决:两个选择。一是把前端文件作为Flask模板渲染,让页面和API同源,最简单;二是给Flask添加CORS支持:
from flask_cors import CORS CORS(app)如果用了Nginx,也可以在后端直接配add_header Access-Control-Allow-Origin *。注意:加了CORS之后,Coze的API Key仍然不要放到前端,这个防护不能丢。
6. 最后的一个技巧:会话恢复与断线重连
到这里,基础对话页已经能跑了。最后我建议你补一个不起眼但很影响体验的能力:会话恢复。
用户刷新页面后,对话记录全没了,这种情况在手机上尤其常见。做法很简单:前端把conversation_id存进localStorage,页面加载时读出来,这样用户刷新之后,Bot还记得之前聊过什么。
function saveConversation(cid) { if (cid) localStorage.setItem('coze_conv', cid); } function loadConversation() { return localStorage.getItem('coze_conv'); }在收到conversation.message.completed事件时调用saveConversation(conversationId),页面初始化时调用loadConversation()赋值给变量。这样用户关掉浏览器再回来,上下文还在,体验从“一次性工具”变成“可持续对话”。
断线重连则是另一个高频场景:用户网络抖动,流式输出到一半中断。前端fetch抛异常后,目前只会把错误文本显示到气泡里,没有重试机制。我踩过这个坑后养成了一个习惯:后端在转发时缓存最近一条完整的回复文本,前端检测到连接中断时,提供一个“点这里重新加载刚才的回复”的按钮,而不是让用户重新打字。
具体做法不复杂:后端把完整的回复存入Redis或内存,前端在出错时用GET /api/message/{conversation_id}拉缓存。这个机制在弱网环境下救了很多次场。但要注意,不能自动重发用户消息,否则用户以为发了一次,实际上业务系统记了两次,容易出乱子。
这三样东西——会话持久化、断线可恢复、流式不卡顿——做好了,你的Coze智能体对话页面在体验上就和原生客服系统没有差距了。希望帮到你。
本文还有配套的精品资源,点击获取