Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions agent/compact.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ func EstimatePromptTokens(workspace, skillCatalog, summary string, history []Cha
// [lastSystemPrompt] + history[:keepStart] + [尾部压缩指令],并带上 lastToolSpecsJSON 还原的
// 工具集 —— 这串前缀正是上次缓存下来的,几乎全命中,只有尾部指令是 miss。
// lastSystemPrompt 为空(无快照)时退回冷路径:compressionPrompt 当 system + 拍平历史。
func RunCompression(lastSystemPrompt, lastToolSpecsJSON string, history []ChatMessage, entry ModelEntry, ctxWin int) (
func RunCompression(lastSystemPrompt, lastToolSpecsJSON string, history []ChatMessage, entry ModelEntry, ctxWin int, focusHint string) (
summary string, cutIdx int, compressedTurns int, err error) {

// 轮数按 isTurnBoundary(user 或 assistant)计:一个 user 消息 + 几十轮工具调用同样是几十轮对话,
Expand Down Expand Up @@ -332,17 +332,25 @@ func RunCompression(lastSystemPrompt, lastToolSpecsJSON string, history []ChatMe
convo := make([]ChatMessage, 0, keepStart+2)
convo = append(convo, ChatMessage{Role: "system", Content: lastSystemPrompt})
convo = append(convo, history[:keepStart]...)
convo = append(convo, ChatMessage{Role: "user", Content: warmCompressInstruction})
instruction := warmCompressInstruction
if focusHint != "" {
instruction = fmt.Sprintf("%s\n\n**压缩侧重点**: 请重点关注与[%s]相关的内容, 保留相关决策和上下文; 与侧重点无关的内容可以更激进地压缩。", instruction, focusHint)
}
convo = append(convo, ChatMessage{Role: "user", Content: instruction})
toolSpecs := UnmarshalToolSpecs(lastToolSpecsJSON)
summary, err = CallWithTools(ctx, entry.APIKey, entry.BaseURL, entry.Model, convo, toolSpecs, summaryMax)
} else {
// 冷路径:无快照,拍平历史走独立 system(必 miss,但正确)。
cp := compressionPrompt
if focusHint != "" {
cp = fmt.Sprintf("%s\n\n**压缩侧重点**: 请重点关注与[%s]相关的内容, 保留相关决策和上下文; 与侧重点无关的内容可以更激进地压缩。", cp, focusHint)
}
var inputBuf strings.Builder
for _, msg := range history[:keepStart] {
inputBuf.WriteString("[" + msg.Role + "]\n" + msg.Content + "\n\n")
}
convo := []ChatMessage{
{Role: "system", Content: compressionPrompt},
{Role: "system", Content: cp},
{Role: "user", Content: inputBuf.String()},
}
summary, err = CallOnce(ctx, entry.APIKey, entry.BaseURL, entry.Model, convo, summaryMax)
Expand Down
4 changes: 2 additions & 2 deletions agent/compact_cooldown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestRunCompression_TooFewTurnsSentinel(t *testing.T) {
{Role: "user", Content: "一"},
{Role: "assistant", Content: "回一"},
} // 2 轮(user + assistant),不多于要保留的 keepRecentTurns
_, _, _, err := RunCompression("", "", hist, ModelEntry{ContextWindow: 100000}, 100000)
_, _, _, err := RunCompression("", "", hist, ModelEntry{ContextWindow: 100000}, 100000, "")
if !errors.Is(err, ErrCompactTooFewTurns) {
t.Fatalf("2 轮应返回 ErrCompactTooFewTurns 哨兵, got %v", err)
}
Expand All @@ -34,7 +34,7 @@ func TestRunCompression_SingleUserLongTurnNotRejected(t *testing.T) {
hist = append(hist, asstCall(id, "Bash", `{"command":"go test"}`), toolMsg(id, "Bash", body))
}
// BaseURL 为空 → 摘要请求在本地就失败;这里只关心它已越过轮数 / 切点守卫。
_, _, _, err := RunCompression("sys", "[]", hist, ModelEntry{ContextWindow: 20000}, 20000)
_, _, _, err := RunCompression("sys", "[]", hist, ModelEntry{ContextWindow: 20000}, 20000, "")
if errors.Is(err, ErrCompactTooFewTurns) {
t.Fatal("单个 user 长任务轮不应再被判成轮数不足")
}
Expand Down
2 changes: 1 addition & 1 deletion agent/compact_live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func TestLive_RunCompressionSucceeds(t *testing.T) {
}
t.Logf("历史 ≈ %d tokens", EstimateHistoryTokens(hist))

summary, cutIdx, turns, err := RunCompression("", "", hist, entry, ctxWin)
summary, cutIdx, turns, err := RunCompression("", "", hist, entry, ctxWin, "")
if err != nil {
t.Fatalf("❌ 真实压缩失败(正常路径不该失败): %v", err)
}
Expand Down
33 changes: 33 additions & 0 deletions agent/embedder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package agent

// Embedder 生成文本的语义向量, 用于主题相似度计算。
// 两种实现: TF-IDF (稀疏, 零依赖) 和 ONNX (稠密, 语义级)。
type Embedder interface {
// Embed 返回文本的语义向量。
Embed(text string) map[string]float64
// Name 返回嵌入器名称。
Name() string
}

// EmbedderType 嵌入器类型。
type EmbedderType string

const (
EmbedderTFIDF EmbedderType = "tfidf" // 默认: TF-IDF 稀疏向量
EmbedderONNX EmbedderType = "onnx" // ONNX Sentence Embeddings
)

// NewEmbedder 创建嵌入器实例。
// t 为类型, cacheDir 为模型缓存目录(仅 ONNX 需要)。
func NewEmbedder(t EmbedderType, cacheDir string) (Embedder, error) {
switch t {
case EmbedderONNX:
emb, err := newONNXEmbedder(cacheDir)
if err != nil {
return nil, err
}
return emb, nil
default:
return newTFIDFEmbedder(), nil
}
}
Loading