后端: 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 的使用边界。
113 lines
3.3 KiB
Go
113 lines
3.3 KiB
Go
package agentsvc
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log"
|
||
"strings"
|
||
|
||
infrallm "github.com/LoveLosita/smartflow/backend/infra/llm"
|
||
newagentprompt "github.com/LoveLosita/smartflow/backend/newAgent/prompt"
|
||
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||
)
|
||
|
||
const reasoningSummaryMaxTokens = 700
|
||
|
||
type reasoningSummaryLLMResponse struct {
|
||
ShortSummary string `json:"short_summary"`
|
||
DetailSummary string `json:"detail_summary"`
|
||
}
|
||
|
||
// makeReasoningSummaryFunc 把便宜模型封装成 stream 层可注入的摘要函数。
|
||
//
|
||
// 职责边界:
|
||
// 1. service 层负责选择模型与 prompt,stream 层只负责调度和闸门;
|
||
// 2. 这里不持久化摘要,持久化统一走 ChunkEmitter 的 extra hook;
|
||
// 3. 摘要失败时返回 error,由 ReasoningDigestor 吞掉并等待下一次水位线/Flush 兜底。
|
||
func (s *AgentService) makeReasoningSummaryFunc(client *infrallm.Client) newagentstream.ReasoningSummaryFunc {
|
||
if client == nil {
|
||
return nil
|
||
}
|
||
|
||
return func(ctx context.Context, input newagentstream.ReasoningSummaryInput) (newagentstream.StreamThinkingSummaryExtra, error) {
|
||
previousSummary := ""
|
||
if input.PreviousSummary != nil {
|
||
previousSummary = input.PreviousSummary.DetailSummary
|
||
if strings.TrimSpace(previousSummary) == "" {
|
||
previousSummary = input.PreviousSummary.ShortSummary
|
||
}
|
||
}
|
||
|
||
messages := newagentprompt.BuildReasoningSummaryMessages(newagentprompt.ReasoningSummaryPromptInput{
|
||
FullReasoning: input.FullReasoning,
|
||
DeltaReasoning: input.DeltaReasoning,
|
||
PreviousSummary: previousSummary,
|
||
CandidateSeq: input.CandidateSeq,
|
||
Final: input.Final,
|
||
DurationSeconds: input.DurationSeconds,
|
||
})
|
||
|
||
resp, rawResult, err := infrallm.GenerateJSON[reasoningSummaryLLMResponse](
|
||
ctx,
|
||
client,
|
||
messages,
|
||
infrallm.GenerateOptions{
|
||
Temperature: 0.1,
|
||
MaxTokens: reasoningSummaryMaxTokens,
|
||
Thinking: infrallm.ThinkingModeDisabled,
|
||
Metadata: map[string]any{
|
||
"stage": "reasoning_summary",
|
||
"candidate_seq": input.CandidateSeq,
|
||
"final": input.Final,
|
||
},
|
||
},
|
||
)
|
||
if err != nil {
|
||
log.Printf("[WARN] reasoning 摘要模型调用失败 seq=%d final=%v err=%v raw=%s",
|
||
input.CandidateSeq,
|
||
input.Final,
|
||
err,
|
||
truncateReasoningSummaryRaw(rawResult),
|
||
)
|
||
return newagentstream.StreamThinkingSummaryExtra{}, err
|
||
}
|
||
|
||
summary := newagentstream.StreamThinkingSummaryExtra{
|
||
ShortSummary: strings.TrimSpace(resp.ShortSummary),
|
||
DetailSummary: limitReasoningDetailSummary(
|
||
resp.DetailSummary,
|
||
newagentprompt.ReasoningSummaryDetailRuneLimit(input.FullReasoning, input.DeltaReasoning),
|
||
),
|
||
}
|
||
if summary.ShortSummary == "" && summary.DetailSummary == "" {
|
||
return newagentstream.StreamThinkingSummaryExtra{}, errors.New("reasoning 摘要模型返回空摘要")
|
||
}
|
||
return summary, nil
|
||
}
|
||
}
|
||
|
||
func limitReasoningDetailSummary(text string, maxRunes int) string {
|
||
text = strings.TrimSpace(text)
|
||
if text == "" || maxRunes <= 0 {
|
||
return text
|
||
}
|
||
|
||
runes := []rune(text)
|
||
if len(runes) <= maxRunes {
|
||
return text
|
||
}
|
||
return string(runes[:maxRunes])
|
||
}
|
||
|
||
func truncateReasoningSummaryRaw(raw *infrallm.TextResult) string {
|
||
if raw == nil {
|
||
return ""
|
||
}
|
||
text := strings.TrimSpace(raw.Text)
|
||
runes := []rune(text)
|
||
if len(runes) <= 200 {
|
||
return text
|
||
}
|
||
return string(runes[:200]) + "..."
|
||
}
|