Version: 0.9.53.dev.260429

后端:
1. 流式思考链路从 raw reasoning_content 切到 `thinking_summary` 摘要协议,补齐摘要 prompt、digestor 与 Lite 压缩链路,plan / execute / fallback 统一改为“只出摘要、不透原始推理”,正文开始后自动关停摘要流。
2. thinking_summary 打通 timeline / SSE / outbox 持久化闭环,只落 detail_summary 与必要 metadata,并补强 seq 自检、冲突幂等识别与补 seq 回填,提升重放恢复稳定性。
3. 会话历史口径继续收紧,assistant 正文与时间线不再回写 raw reasoning_content,仅保留正文与思考耗时,避免刷新恢复时再次暴露内部推理文本。

前端:
4. 助手页开始接入 thinking_summary 实时流与历史恢复,补齐短摘要状态、长摘要折叠区、正文开流后自动收口,并增加调试入口用于协议联调与验收。
5. 当前前端助手页仍是残次过渡态,本版先以 thinking_summary 协议接通和基础渲染为主,样式、交互与细节体验暂未收平,下一版集中修复。

仓库:
6. 补充 thinking_summary 对接说明,明确 SSE 协议、timeline 恢复口径与 short/detail summary 的使用边界。
This commit is contained in:
Losita
2026-04-29 01:00:38 +08:00
parent d89e2830a9
commit f81f137791
21 changed files with 8566 additions and 229 deletions

View File

@@ -0,0 +1,128 @@
package newagentprompt
import (
"encoding/json"
"fmt"
"strings"
"unicode/utf8"
"github.com/cloudwego/eino/schema"
)
const (
reasoningSummaryMaxFullRunes = 6000
reasoningSummaryMaxDeltaRunes = 1800
)
// ReasoningSummaryPromptInput 描述一次“思考摘要”模型调用所需的最小输入。
//
// 职责边界:
// 1. 只承载摘要模型需要看的文本与运行态,不绑定 stream 包的 DTO避免 prompt 层反向依赖输出协议;
// 2. FullReasoning 会在构造 prompt 时只保留尾部,避免长时间思考把便宜模型上下文撑爆;
// 3. PreviousSummary 只作为连续摘要的参考,不要求模型逐字继承。
type ReasoningSummaryPromptInput struct {
FullReasoning string
DeltaReasoning string
PreviousSummary string
CandidateSeq int
Final bool
DurationSeconds float64
}
type reasoningSummaryPromptPayload struct {
CandidateSeq int `json:"candidate_seq"`
Final bool `json:"final"`
DurationSeconds float64 `json:"duration_seconds"`
PreviousSummary string `json:"previous_summary,omitempty"`
RecentReasoning string `json:"recent_reasoning,omitempty"`
DeltaReasoning string `json:"delta_reasoning,omitempty"`
SourceTextRunes int `json:"source_text_runes,omitempty"`
MaxDetailSummaryRunes int `json:"max_detail_summary_runes,omitempty"`
}
// BuildReasoningSummaryMessages 构造思考摘要模型调用的 messages。
//
// 步骤说明:
// 1. system prompt 明确“只做用户可见摘要”,禁止复述原始思考链和内部推理细节;
// 2. user prompt 使用 JSON 承载输入,便于后续扩展字段且减少模型误读;
// 3. 长文本只保留尾部窗口,保证异步摘要请求稳定、便宜、可控。
func BuildReasoningSummaryMessages(input ReasoningSummaryPromptInput) []*schema.Message {
recentReasoning := trimRunesFromEnd(input.FullReasoning, reasoningSummaryMaxFullRunes)
deltaReasoning := trimRunesFromEnd(input.DeltaReasoning, reasoningSummaryMaxDeltaRunes)
payload := reasoningSummaryPromptPayload{
CandidateSeq: input.CandidateSeq,
Final: input.Final,
DurationSeconds: input.DurationSeconds,
PreviousSummary: strings.TrimSpace(input.PreviousSummary),
RecentReasoning: recentReasoning,
DeltaReasoning: deltaReasoning,
SourceTextRunes: reasoningSummarySourceRunes(recentReasoning, deltaReasoning),
MaxDetailSummaryRunes: ReasoningSummaryDetailRuneLimit(input.FullReasoning, input.DeltaReasoning),
}
raw, err := json.MarshalIndent(payload, "", " ")
if err != nil {
raw = []byte(fmt.Sprintf(`{"recent_reasoning":%q}`, trimRunesFromEnd(input.FullReasoning, reasoningSummaryMaxFullRunes)))
}
return []*schema.Message{
schema.SystemMessage(buildReasoningSummarySystemPrompt()),
schema.UserMessage("请把下面的模型思考内容整理成用户可见的进度摘要。\n输入\n" + string(raw)),
}
}
func buildReasoningSummarySystemPrompt() string {
return strings.TrimSpace(`你是 SmartMate 的“思考摘要器”。你的任务是把模型内部 reasoning 整理成用户可见的进度摘要。
输出必须是严格 JSON 对象:
{
"short_summary": "8到18个汉字的短摘要",
"detail_summary": "不超过 max_detail_summary_runes 个字的展开摘要"
}
规则:
1. 只描述“正在做什么”和“目前推进到哪一步”,不要复述、引用或暴露原始思考链。
2. 不输出 markdown不输出代码块不解释 JSON 以外的内容。
3. short_summary 要短、稳定、适合前端几秒刷新一次。
4. detail_summary 不按固定句数限制,而按输入长度控制:字数必须小于等于 max_detail_summary_runes不需要凑满上限信息密度优先。
5. detail_summary 仍然面向用户,不写内部推理细节、隐含假设链、逐步演算。
6. 若输入为空或噪声较多,用保守摘要,例如“正在整理思路”“正在核对可用信息”。
7. final=true 时detail_summary 用完成态语气,说明思考已收拢到下一步答复或动作。`)
}
// ReasoningSummaryDetailRuneLimit 返回 detail_summary 的最大字数。
//
// 职责边界:
// 1. 与 BuildReasoningSummaryMessages 使用同一套输入窗口,避免 prompt 提示和服务端兜底口径不一致;
// 2. 上限取“提供给摘要模型的主要文本段”的一半,并向上取整,适配极短文本;
// 3. 返回 0 表示没有有效输入文本,调用方不应做硬裁剪。
func ReasoningSummaryDetailRuneLimit(fullReasoning, deltaReasoning string) int {
recentReasoning := trimRunesFromEnd(fullReasoning, reasoningSummaryMaxFullRunes)
delta := trimRunesFromEnd(deltaReasoning, reasoningSummaryMaxDeltaRunes)
sourceRunes := reasoningSummarySourceRunes(recentReasoning, delta)
if sourceRunes <= 0 {
return 0
}
return (sourceRunes + 1) / 2
}
func reasoningSummarySourceRunes(recentReasoning, deltaReasoning string) int {
recentReasoning = strings.TrimSpace(recentReasoning)
if recentReasoning != "" {
return utf8.RuneCountInString(recentReasoning)
}
return utf8.RuneCountInString(strings.TrimSpace(deltaReasoning))
}
func trimRunesFromEnd(text string, maxRunes int) string {
text = strings.TrimSpace(text)
if text == "" || maxRunes <= 0 {
return ""
}
runes := []rune(text)
if len(runes) <= maxRunes {
return text
}
return string(runes[len(runes)-maxRunes:])
}