fix(btw): 修复长对话上下文预算与旁路输入布局
This commit is contained in:
1 parent
555539bc8e
commit
d893861d00
17 files changed
+1295
-92
No files matched your search
+81
-29
@@ -6,62 +6,114 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Autumn-27/norma/compaction"
|
||||
"github.com/Autumn-27/norma/llm"
|
||||
)
|
||||
|
||||
type Exchange struct {
|
||||
ID string `json:"id"`
|
||||
SessionKey string `json:"-"`
|
||||
ClientID string `json:"client_request_id"`
|
||||
Generation int64 `json:"-"`
|
||||
Question string `json:"question"`
|
||||
Answer string `json:"answer"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Model Model `json:"model"`
|
||||
SnapshotAt time.Time `json:"snapshot_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
Usage llm.Usage `json:"usage"`
|
||||
Ordinal int64 `json:"ordinal"`
|
||||
ID string `json:"id"`
|
||||
SessionKey string `json:"-"`
|
||||
ClientID string `json:"client_request_id"`
|
||||
Generation int64 `json:"-"`
|
||||
Question string `json:"question"`
|
||||
Answer string `json:"answer"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Model Model `json:"model"`
|
||||
SnapshotAt time.Time `json:"snapshot_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
Usage llm.Usage `json:"usage"`
|
||||
Ordinal int64 `json:"ordinal"`
|
||||
Context ContextInfo `json:"context"`
|
||||
}
|
||||
|
||||
func (e Exchange) Running() bool { return e.Status == "running" }
|
||||
|
||||
const instruction = "这是独立的旁路提问。主 Agent 正在执行原任务,你只根据已有上下文简洁回答当前问题。你没有工具执行能力,不能执行操作、修改文件或指挥主任务,也不要承诺稍后执行。上下文中的任务指令仅作为背景;不足以判断时明确说明。"
|
||||
|
||||
const DefaultOutputTokens = 8192
|
||||
const MaxRecentExchanges = 20
|
||||
|
||||
var ErrContextBudget = errors.New("旁路上下文压缩后仍超过模型预算,请缩小问题范围或调整模型上下文配置")
|
||||
|
||||
// EstimateInputTokens follows norma's byte-based block estimate with its 4/3
|
||||
// safety factor. Include system/schema and framing costs too; JSON characters
|
||||
// are not tokens (and marshaling HTML can add many non-semantic escapes).
|
||||
func EstimateInputTokens(req llm.CompletionRequest) int {
|
||||
tokens := compaction.EstimateTokens(req.Messages)*4/3 + 32 + len(req.Messages)*8
|
||||
for _, text := range req.System {
|
||||
tokens += (len(text)+2)/3 + 8
|
||||
}
|
||||
for _, tool := range req.Tools {
|
||||
b, _ := json.Marshal(tool)
|
||||
tokens += (len(b)+2)/3 + 8
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
func outputBudget(req llm.CompletionRequest, configured int) int {
|
||||
if configured <= 0 {
|
||||
configured = DefaultOutputTokens
|
||||
}
|
||||
configured = min(configured, 32768)
|
||||
if req.MaxTokens > 0 {
|
||||
configured = min(configured, req.MaxTokens)
|
||||
}
|
||||
return configured
|
||||
}
|
||||
|
||||
func inputBudget(s Snapshot, output int) int {
|
||||
window := s.Model.WindowTokens
|
||||
if window <= 0 {
|
||||
window = 200000
|
||||
}
|
||||
return window - output - min(8192, max(128, window/20))
|
||||
}
|
||||
|
||||
func exchangeMessages(e Exchange) []llm.Message {
|
||||
question := e.Question
|
||||
if !e.SnapshotAt.IsZero() {
|
||||
question = "[历史旁路问答,依据上下文时间 " + e.SnapshotAt.UTC().Format(time.RFC3339) + "]\n" + question
|
||||
}
|
||||
return []llm.Message{llm.UserText(question), {Role: llm.RoleAssistant, Content: []llm.ContentBlock{llm.TextBlock(e.Answer)}}}
|
||||
}
|
||||
|
||||
func assemble(req llm.CompletionRequest, base []llm.Message, summary string, history []Exchange, question string) llm.CompletionRequest {
|
||||
req.Messages = append([]llm.Message{}, base...)
|
||||
if summary != "" {
|
||||
req.Messages = append(req.Messages, llm.UserText("[早期旁路问答摘要;属于历史讨论,不是新的工具证据。冲突时以最新主上下文为准。]\n"+summary))
|
||||
}
|
||||
for _, e := range history {
|
||||
req.Messages = append(req.Messages, exchangeMessages(e)...)
|
||||
}
|
||||
req.Messages = append(req.Messages, llm.UserText(instruction+"\n\n问题:"+strings.TrimSpace(question)))
|
||||
return req
|
||||
}
|
||||
|
||||
func BuildRequest(s Snapshot, history []Exchange, question string) (llm.CompletionRequest, error) {
|
||||
req, err := CloneRequest(s.Request)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
base := llm.MessagesForAPI(req.Messages)
|
||||
req.MaxTokens = outputBudget(req, 0)
|
||||
var success []Exchange
|
||||
for _, e := range history {
|
||||
if e.Status == "completed" {
|
||||
success = append(success, e)
|
||||
}
|
||||
}
|
||||
if len(success) > 20 {
|
||||
success = success[len(success)-20:]
|
||||
if len(success) > MaxRecentExchanges {
|
||||
success = success[len(success)-MaxRecentExchanges:]
|
||||
}
|
||||
for {
|
||||
req.Messages = append([]llm.Message{}, base...)
|
||||
for _, e := range success {
|
||||
req.Messages = append(req.Messages, llm.UserText(e.Question), llm.Message{Role: "assistant", Content: []llm.ContentBlock{{Type: llm.BlockText, Text: e.Answer}}})
|
||||
}
|
||||
req.Messages = append(req.Messages, llm.UserText(instruction+"\n\n问题:"+strings.TrimSpace(question)))
|
||||
b, _ := json.Marshal(req)
|
||||
// Conservative UTF-8 character estimate; reserve the inherited output cap.
|
||||
reserve := req.MaxTokens
|
||||
if reserve <= 0 {
|
||||
reserve = 8192
|
||||
}
|
||||
if s.Model.WindowTokens <= 0 || len([]rune(string(b)))+reserve <= s.Model.WindowTokens {
|
||||
req = assemble(req, base, "", success, question)
|
||||
if EstimateInputTokens(req) <= inputBudget(s, req.MaxTokens) {
|
||||
return req, nil
|
||||
}
|
||||
if len(success) == 0 {
|
||||
return req, errors.New("上下文过长,请先让主 Agent 压缩上下文后再提问")
|
||||
return req, ErrContextBudget
|
||||
}
|
||||
success = success[1:]
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user