SolonCode Web 聊天界面底部的工具栏提供了三个操作:复制上一条回复、重新运行(Re-run)和继续运行(Continue)。后两个按钮看起来都像“让模型再执行一次”,但它们处理旧回答的方式完全相反:Re-run 删除旧回答并重新生成,Continue 保留旧回答并在其后追加内容。理解这一区别,关键不在按钮文案,而在消息状态和上下文如何变化。
两个按钮解决的是不同问题
假设用户输入了一条任务指令,系统已经生成回答 A。
点击 Re-run 时,界面会删除回答 A,再使用相同输入启动一轮生成。完成后,回答 B 占据原来回答 A 的位置。这个操作适合处理以下情况:
- 回答方向错误,希望彻底重来;
- 生成过程异常中断,需要从原输入重新执行;
- 想利用模型输出的不确定性,获得另一种完整答案;
- 不希望旧回答继续进入后续处理流程。
点击 Continue 时,回答 A 会被保留,系统在现有结果之后继续生成。它更适合:
- 回答因长度限制而停止;
- 代码只生成了一半;
- 任务已经走在正确方向上,只需要补齐剩余内容;
- 希望模型基于已有结果继续推导,而不是推翻重做。
可以把两者概括为:
Re-run: 输入 -> 删除旧回答 -> 重新生成完整回答
Continue: 输入 + 旧回答 -> 保留旧回答 -> 生成追加内容
真正的差异在状态转换
从聊天状态的角度看,Re-run 是“替换”,Continue 是“追加”。二者不能只共用一个普通的 sendMessage(),然后靠界面决定是否隐藏旧内容,因为发送给模型的上下文也可能不同。
基于来源描述,可以这样实践一个最小状态模型。下面的 Node.js 示例不依赖第三方包,用模拟模型展示两种操作对消息列表的影响。将代码保存为 demo.mjs,然后执行 node demo.mjs:
const initialMessages = [
{ id: "u1", role: "user", content: "生成一个分页查询函数" },
{ id: "a1", role: "assistant", content: "旧回答:函数只写到一半……" }
];
async function callModel(messages, mode) {
const lastUser = [...messages].reverse().find((m) => m.role === "user");
const previousAnswer = [...messages]
.reverse()
.find((m) => m.role === "assistant");
if (mode === "continue") {
return `追加内容:基于“${previousAnswer.content}”继续完成分页逻辑。`;
}
return `新回答:重新处理“${lastUser.content}”,生成完整分页实现。`;
}
async function rerun(messages) {
const next = [...messages];
if (next.at(-1)?.role === "assistant") {
next.pop();
}
const content = await callModel(next, "rerun");
next.push({ id: crypto.randomUUID(), role: "assistant", content });
return next;
}
async function continueRun(messages) {
const next = [...messages];
const content = await callModel(next, "continue");
next.push({ id: crypto.randomUUID(), role: "assistant", content });
return next;
}
console.log("Re-run 结果:");
console.table(await rerun(initialMessages));
console.log("Continue 结果:");
console.table(await continueRun(initialMessages));
这个示例是对交互语义的简化建模,并不代表 SolonCode 的内部源码实现。它强调了一个重要边界:Re-run 调用模型前先移除旧回答,而 Continue 调用模型时保留旧回答,使追加生成能够参考已有内容。
前端不能只处理“气泡”
如果在真实 Web 应用中实现这两个按钮,需要同时考虑界面状态、持久化记录和流式响应。
Re-run 的典型流程可以这样设计:
- 找到需要替换的 assistant 消息;
- 暂时禁用 Re-run 和 Continue,避免重复请求;
- 从本地状态或服务端会话中移除旧回答;
- 使用对应的用户输入重新发起请求;
- 创建新的回答气泡并接收流式片段;
- 请求失败时恢复旧回答,或提供明确的重试入口。
Continue 的流程则不同:
- 保留当前回答;
- 将已有对话作为上下文发给模型;
- 新建追加消息,或明确地把流式内容拼接到当前消息;
- 记录本次操作为 continue,避免后端误判为新问题;
- 防止用户连续点击产生多个并发追加任务。
这里有一个产品层面的决定:Continue 的结果究竟应该成为新气泡,还是拼接进原气泡?来源描述强调“保留旧回答,继续追加内容”,但没有给出数据层的具体结构。工程上更建议保留独立消息记录,再在视觉上连续展示。这样更容易撤销、审计和处理失败重试。
流式生成中的失败处理
这两个操作遇到网络中断时,风险也不同。
Re-run 通常先删除旧回答。如果新请求失败,而客户端又没有保存快照,用户会同时失去旧结果和新结果。因此,实现时应在请求成功建立之前保留旧消息快照,失败后执行回滚。
Continue 不会删除旧回答,但可能留下半截追加内容。应用需要给追加消息标记状态,例如:
{
id: "a2",
role: "assistant",
content: "正在追加的部分内容……",
operation: "continue",
status: "interrupted"
}
有了 operation 和 status,界面才能准确提供“继续该追加任务”“重新运行原问题”或“删除未完成内容”等操作,而不是把所有失败都压缩成一个含义模糊的重试按钮。
落地时检查这几件事
实现或使用这两个操作时,可以按下面的清单确认行为:
- Re-run 是否真的从模型上下文中移除了旧回答,而不只是隐藏气泡;
- Re-run 失败后能否恢复旧回答;
- Continue 是否携带了已有回答所需的上下文;
- 两个按钮在流式生成期间是否被禁用;
- 重复点击是否会创建并发请求;
- 服务端是否记录操作类型、父消息和请求标识;
- Continue 产生的半截内容是否可以继续、删除或重试;
- 重新生成后,后续对话引用的是新回答还是已删除的旧回答。
Re-run 和 Continue 的界面差异只有一个按钮,但数据语义并不相近。前者把当前回答视为可替换结果,后者把它视为下一轮生成的起点。把这两个状态转换分开建模,才能避免上下文污染、消息丢失和重复生成。