Wire the intent slow lane behind settings flags (I2 WP3) #83
Vendored
+1
-1
Submodule deps/agent-intent updated: f57645c645...343b1e6d9a
+17
-2
@@ -36,6 +36,7 @@ import (
|
||||
"agent-system-backend/internal/config"
|
||||
"agent-system-backend/internal/hostapi"
|
||||
"agent-system-backend/internal/intentgate"
|
||||
"agent-system-backend/internal/intentsem"
|
||||
"agent-system-backend/internal/pgbus"
|
||||
"agent-system-backend/internal/plugins"
|
||||
"agent-system-backend/internal/profiles"
|
||||
@@ -330,12 +331,26 @@ func Run(opt Options) error {
|
||||
|
||||
// 检索管线(4.12):分块+全文+(配置时)向量混合与 rerank;未配置自动退化为全文。
|
||||
pipe := rag.NewPipeline(st, setSvc, nil, nil)
|
||||
pipe.SetEmbedder(rag.SwitchingEmbedder{HTTP: rag.NewHTTPEmbedder(pipe.EmbeddingRoute), Provider: func() string {
|
||||
embedder := rag.SwitchingEmbedder{HTTP: rag.NewHTTPEmbedder(pipe.EmbeddingRoute), Provider: func() string {
|
||||
return setSvc.Effective(store.OwnerDefault, "").GetString("models.embedding.provider", "")
|
||||
}})
|
||||
}}
|
||||
pipe.SetEmbedder(embedder)
|
||||
pipe.SetReranker(rag.NewHTTPReranker(pipe.RerankRoute))
|
||||
mgr.SetRetriever(pipe)
|
||||
mgr.SetIndexer(pipe)
|
||||
|
||||
// 意图慢通道(I2):语义向量裁决 + 轻量模型命令生成。
|
||||
// 两个开关都默认关——此时识别行为与 I1 完全一致(fail-closed 分层);
|
||||
// 开启语义需已配置 embedding 端点,开启生成需可用的轻量/主模型档。
|
||||
if setSvc.Effective(store.OwnerDefault, "").GetBool("runtime.intentSemantic", false) {
|
||||
gate.SetSemantic(intentsem.EmbeddingScorer{Provider: embedder}, 0)
|
||||
}
|
||||
if setSvc.Effective(store.OwnerDefault, "").GetBool("runtime.intentGenerator", false) {
|
||||
effNow := setSvc.Effective(store.OwnerDefault, "")
|
||||
light := runtime.ResolveLightweightModel(core.AgentProfile{}, effNow)
|
||||
client := &agentllm.Client{BaseURL: light.Base, APIKey: light.Key, HTTPClient: &http.Client{}}
|
||||
gate.SetGenerator(intentsem.LightGenerator{Stream: client.Stream, Model: light.Model})
|
||||
}
|
||||
// fs 预留能力沙箱根:<DataDir>/workspaces/<owner>/<conversationID>/。
|
||||
mgr.SetWorkspaceRoot(filepath.Join(opt.DataDir, "workspaces"))
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"agent-system-backend/internal/settings"
|
||||
core "agent-system-core"
|
||||
)
|
||||
|
||||
// 慢通道开关默认关(行为与 I1 一致的机器证明):
|
||||
// 两条 runtime.intentSemantic / runtime.intentGenerator 必须注册且默认 false。
|
||||
func TestIntentSlowLaneFlagsDefaultOff(t *testing.T) {
|
||||
seen := map[string]core.SettingDesc{}
|
||||
for _, d := range settings.Schema() {
|
||||
seen[d.Key] = d
|
||||
}
|
||||
for _, key := range []string{"runtime.intentSemantic", "runtime.intentGenerator"} {
|
||||
d, ok := seen[key]
|
||||
if !ok {
|
||||
t.Fatalf("设置未注册:%s", key)
|
||||
}
|
||||
if d.Type != core.SettingBool {
|
||||
t.Fatalf("%s 类型应为 bool,得到 %v", key, d.Type)
|
||||
}
|
||||
if def, isBool := d.Default.(bool); !isBool || def {
|
||||
t.Fatalf("%s 必须默认 false,得到 %v", key, d.Default)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,12 +51,47 @@ type Gate struct {
|
||||
rec *agentintent.FunnelRecognizer
|
||||
specs int
|
||||
specsByID map[string]agentintent.IntentSpec
|
||||
// 慢通道(I2):nil = 不启用(默认,行为与 I1 一致)。
|
||||
sem agentintent.SemanticScorer
|
||||
semThresh float64
|
||||
gen agentintent.CommandGenerator
|
||||
}
|
||||
|
||||
func New() *Gate {
|
||||
return &Gate{rec: agentintent.NewRecognizer(agentintent.Options{}, nil, nil)}
|
||||
}
|
||||
|
||||
// SetSemantic 接线语义打分器(慢通道中位)。识别器随之重建;
|
||||
// 传 nil 关闭语义层。阈值 ≤0 用库默认(0.8)。
|
||||
func (g *Gate) SetSemantic(s agentintent.SemanticScorer, threshold float64) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.sem, g.semThresh = s, threshold
|
||||
g.rebuildRecognizerLocked()
|
||||
}
|
||||
|
||||
// SetGenerator 接线模型命令生成器(慢通道末位)。识别器随之重建;
|
||||
// 传 nil 关闭生成层。
|
||||
func (g *Gate) SetGenerator(gen agentintent.CommandGenerator) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.gen = gen
|
||||
g.rebuildRecognizerLocked()
|
||||
}
|
||||
|
||||
// rebuildRecognizerLocked 按当前慢通道配置重建识别器(调用方持写锁)。
|
||||
func (g *Gate) rebuildRecognizerLocked() {
|
||||
if g.sem == nil && g.gen == nil {
|
||||
g.rec = agentintent.NewRecognizer(agentintent.Options{}, nil, nil)
|
||||
return
|
||||
}
|
||||
g.rec = agentintent.NewRecognizer(agentintent.Options{
|
||||
Semantic: g.sem,
|
||||
SemanticThreshold: g.semThresh,
|
||||
Generator: g.gen,
|
||||
}, nil, nil)
|
||||
}
|
||||
|
||||
// Rebuild 用插件意图 overlay 重建注册表;失败保留上一快照(fail-closed)并返回错误。
|
||||
func (g *Gate) Rebuild(ctx context.Context, overlay []json.RawMessage) error {
|
||||
if len(overlay) == 0 {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
agentintent "agent-intent/intent"
|
||||
)
|
||||
|
||||
func raw(t *testing.T, v any) json.RawMessage {
|
||||
@@ -91,3 +93,86 @@ func TestRecognizeKeepsSlots(t *testing.T) {
|
||||
t.Fatalf("Match wrapper = %s %s", branch, id)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- I2 慢通道接线(默认关;SetSemantic/SetGenerator 开启)----
|
||||
|
||||
// stubScorer 固定返回脚本分(按 examples[0] 查表)。
|
||||
type stubScorer struct{ scores map[string]float64 }
|
||||
|
||||
func (s stubScorer) Score(_ context.Context, _ string, examples []string) (float64, error) {
|
||||
if len(examples) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return s.scores[examples[0]], nil
|
||||
}
|
||||
|
||||
// stubGenerator 固定返回脚本命令。
|
||||
type stubGenerator struct{ cmds []Command }
|
||||
|
||||
func (g stubGenerator) Generate(_ context.Context, _ string, _ []agentintent.CatalogSpec) ([]agentintent.Command, error) {
|
||||
return g.cmds, nil
|
||||
}
|
||||
|
||||
func ambiguousOverlay(t *testing.T) []json.RawMessage {
|
||||
t.Helper()
|
||||
mk := func(id string, ex []string) json.RawMessage {
|
||||
return raw(t, map[string]any{
|
||||
"id": id, "domain": "demo", "version": 1, "priority": 5,
|
||||
"matcher": map[string]any{"includes_any": []string{"模糊词"}},
|
||||
"examples": ex,
|
||||
})
|
||||
}
|
||||
return []json.RawMessage{mk("demo.alpha", []string{"查运行进度"}), mk("demo.beta", []string{"闲聊问候"})}
|
||||
}
|
||||
|
||||
// 语义层:歧义被过阈打分裁决为 matched(默认关时不启用)。
|
||||
func TestGateSemanticResolvesAmbiguous(t *testing.T) {
|
||||
g := New()
|
||||
if err := g.Rebuild(context.Background(), ambiguousOverlay(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 默认(未接线):规则层歧义原样透出。
|
||||
if branch, _ := g.Match(context.Background(), "模糊词"); branch != "ambiguous" {
|
||||
t.Fatalf("默认关:branch = %s", branch)
|
||||
}
|
||||
g.SetSemantic(stubScorer{scores: map[string]float64{"查运行进度": 0.95, "闲聊问候": 0.4}}, 0)
|
||||
branch, top := g.Match(context.Background(), "模糊词")
|
||||
if branch != "matched" || top != "demo.alpha" {
|
||||
t.Fatalf("语义裁决:%s/%s", branch, top)
|
||||
}
|
||||
// 关闭后回到歧义。
|
||||
g.SetSemantic(nil, 0)
|
||||
if branch, _ := g.Match(context.Background(), "模糊词"); branch != "ambiguous" {
|
||||
t.Fatalf("关闭语义层:branch = %s", branch)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成层:fallback 时产出合法 start 命令 → matched + Commands。
|
||||
func TestGateGeneratorProducesCommands(t *testing.T) {
|
||||
g := New()
|
||||
overlay := []json.RawMessage{raw(t, map[string]any{
|
||||
"id": "report.generate", "domain": "report", "version": 1,
|
||||
"description": "生成报告",
|
||||
"matcher": map[string]any{"includes_any": []string{"生成报告"}},
|
||||
"handler": "reportHandler",
|
||||
})}
|
||||
if err := g.Rebuild(context.Background(), overlay); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g.SetGenerator(stubGenerator{cmds: []Command{{
|
||||
Kind: CommandStart,
|
||||
Action: ActionRef{Kind: ActionHandler, ID: "reportHandler"},
|
||||
}}})
|
||||
res := g.Recognize(context.Background(), "完全不相关的一句话")
|
||||
if res.Branch != "matched" || res.ID != "report.generate" {
|
||||
t.Fatalf("生成器命中:%+v", res)
|
||||
}
|
||||
if len(res.Commands) != 1 || res.Commands[0].Kind != CommandStart {
|
||||
t.Fatalf("commands: %+v", res.Commands)
|
||||
}
|
||||
// 关闭后回到 fallback。
|
||||
g.SetGenerator(nil)
|
||||
if branch, _ := g.Match(context.Background(), "完全不相关的一句话"); branch != "fallback" {
|
||||
t.Fatalf("关闭生成层:branch = %s", branch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package intentsem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
agentintent "agent-intent/intent"
|
||||
agentllm "agent-native/runtime/llm"
|
||||
)
|
||||
|
||||
// LightGenerator 用轻量模型实现 agentintent.CommandGenerator
|
||||
// (Rasa 式 LLM command generator:只把相关流程交给生成器)。
|
||||
// 提示词给出能力目录(spec id/描述/槽位键),要求模型输出严格的
|
||||
// 命令 JSON 数组;解析失败或模型不可达都返回错误——库侧约定生成器
|
||||
// 出错时识别保持 fallback(fail-closed)。
|
||||
type LightGenerator struct {
|
||||
// Stream 提供方边界(注入点;测试可脚本化)。
|
||||
Stream agentllm.StreamFunc
|
||||
// Model 是轻量档模型名。
|
||||
Model string
|
||||
// APIBase/APIKey 走 StreamOptions(客户端可按次覆盖)。
|
||||
APIBase string
|
||||
APIKey string
|
||||
// MaxTokens 上限;0 = 1024。
|
||||
MaxTokens int
|
||||
}
|
||||
|
||||
// 生成输出契约:action 与 agentintent.ActionRef 同形(kind/id)。
|
||||
type generatedCommand struct {
|
||||
Kind string `json:"kind"`
|
||||
Action generatedAction `json:"action,omitempty"`
|
||||
Slots map[string]string `json:"slots,omitempty"`
|
||||
}
|
||||
|
||||
type generatedAction struct {
|
||||
Kind string `json:"kind"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// Generate 基于输入与能力目录产出命令列表。目录为空时不调用模型。
|
||||
func (g LightGenerator) Generate(ctx context.Context, input string, catalog []agentintent.CatalogSpec) ([]agentintent.Command, error) {
|
||||
if g.Stream == nil || len(catalog) == 0 || strings.TrimSpace(input) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
maxTok := g.MaxTokens
|
||||
if maxTok <= 0 {
|
||||
maxTok = 1024
|
||||
}
|
||||
msgs := []agentllm.Message{
|
||||
{Role: agentllm.RoleSystem, Content: generatorSystemPrompt(catalog)},
|
||||
{Role: agentllm.RoleUser, Content: input},
|
||||
}
|
||||
ch := g.Stream(ctx, g.Model, msgs, nil, agentllm.StreamOptions{
|
||||
MaxTokens: maxTok, APIBase: g.APIBase, APIKey: g.APIKey,
|
||||
})
|
||||
var sb strings.Builder
|
||||
for ev := range ch {
|
||||
switch ev.Kind {
|
||||
case agentllm.KindTextDelta:
|
||||
sb.WriteString(ev.Text)
|
||||
case agentllm.KindError:
|
||||
if ev.Err != nil {
|
||||
return nil, fmt.Errorf("intentsem: stream: %w", ev.Err)
|
||||
}
|
||||
return nil, fmt.Errorf("intentsem: stream: %s", ev.ErrorMessage)
|
||||
}
|
||||
}
|
||||
return parseCommands(sb.String())
|
||||
}
|
||||
|
||||
// parseCommands 从模型输出解析命令数组:剥代码围栏,取首个 [ 到末个 ],
|
||||
// 严格 JSON。任何形状不符都报错(由库侧 fail-closed 到 fallback)。
|
||||
func parseCommands(raw string) ([]agentintent.Command, error) {
|
||||
text := strings.TrimSpace(raw)
|
||||
text = strings.TrimPrefix(text, "```json")
|
||||
text = strings.TrimPrefix(text, "```")
|
||||
text = strings.TrimSuffix(text, "```")
|
||||
text = strings.TrimSpace(text)
|
||||
lo, hi := strings.Index(text, "["), strings.LastIndex(text, "]")
|
||||
if lo < 0 || hi <= lo {
|
||||
return nil, fmt.Errorf("intentsem: 输出不含命令数组")
|
||||
}
|
||||
var gen []generatedCommand
|
||||
if err := json.Unmarshal([]byte(text[lo:hi+1]), &gen); err != nil {
|
||||
return nil, fmt.Errorf("intentsem: 命令 JSON: %w", err)
|
||||
}
|
||||
out := make([]agentintent.Command, 0, len(gen))
|
||||
for _, c := range gen {
|
||||
out = append(out, agentintent.Command{
|
||||
Kind: agentintent.CommandKind(c.Kind),
|
||||
Action: agentintent.ActionRef{Kind: c.Action.Kind, ID: c.Action.ID},
|
||||
Slots: c.Slots,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// generatorSystemPrompt 渲染能力目录。只暴露 id/描述/槽位键——
|
||||
// 不暴露模式与示例(避免提示词注入借道示例文本进入慢通道)。
|
||||
func generatorSystemPrompt(catalog []agentintent.CatalogSpec) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("你是意图命令生成器。根据用户输入,从下面的能力目录选择并输出命令。\n")
|
||||
sb.WriteString("只输出一个 JSON 数组,不要输出任何其他文字。数组元素形如:\n")
|
||||
sb.WriteString(`[{"kind":"start","action":{"kind":"workflow","id":"<目录中的id>"},"slots":{"<槽位键>":"<值>"}}]` + "\n")
|
||||
sb.WriteString("规则:kind 取 start/set_slot/cancel/clarify/chitchat/handoff 之一;")
|
||||
sb.WriteString("start 必须带 action,action.kind 取 auto/command/workflow/blueprint/handler;")
|
||||
sb.WriteString("action.id 与 slots 的键必须来自目录;用户闲聊用 chitchat(不带 action);")
|
||||
sb.WriteString("无法判断时输出空数组 []。\n能力目录:\n")
|
||||
for _, spec := range catalog {
|
||||
sb.WriteString("- id=" + spec.ID)
|
||||
if spec.Description != "" {
|
||||
sb.WriteString(" 描述=" + spec.Description)
|
||||
}
|
||||
if spec.Handler != "" {
|
||||
sb.WriteString(" handler=" + spec.Handler)
|
||||
}
|
||||
if len(spec.Slots) > 0 {
|
||||
keys := make([]string, 0, len(spec.Slots))
|
||||
for _, sl := range spec.Slots {
|
||||
keys = append(keys, sl.Key)
|
||||
}
|
||||
sb.WriteString(" 槽位=[" + strings.Join(keys, ",") + "]")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package intentsem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
agentintent "agent-intent/intent"
|
||||
agentllm "agent-native/runtime/llm"
|
||||
)
|
||||
|
||||
// fakeEmbedder 返回脚本化向量:texts[i] → vecs[i](循环取模)。
|
||||
type fakeEmbedder struct {
|
||||
vecs [][]float32
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeEmbedder) Embed(_ context.Context, texts []string) ([][]float32, error) {
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
out := make([][]float32, len(texts))
|
||||
for i := range texts {
|
||||
out[i] = f.vecs[i%len(f.vecs)]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func TestEmbeddingScorerMaxCosine(t *testing.T) {
|
||||
// input 与 example2 同向(相似度 1),与 example1 正交(0)。
|
||||
s := EmbeddingScorer{Provider: fakeEmbedder{vecs: [][]float32{
|
||||
{1, 0}, {0, 1}, {2, 0},
|
||||
}}}
|
||||
got, err := s.Score(context.Background(), "输入", []string{"例一", "例二"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != 1 {
|
||||
t.Fatalf("期望最大余弦 1,得到 %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddingScorerErrorPropagates(t *testing.T) {
|
||||
s := EmbeddingScorer{Provider: fakeEmbedder{err: errors.New("boom")}}
|
||||
if _, err := s.Score(context.Background(), "x", []string{"y"}); err == nil {
|
||||
t.Fatal("期望错误上抛(库侧保持 Ambiguous)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddingScorerEmptyExamples(t *testing.T) {
|
||||
s := EmbeddingScorer{Provider: fakeEmbedder{vecs: [][]float32{{1}}}}
|
||||
got, err := s.Score(context.Background(), "x", nil)
|
||||
if err != nil || got != 0 {
|
||||
t.Fatalf("空示例应得 0 分无错误,得到 %v / %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
// scriptedStream 回放固定文本增量后关闭通道。
|
||||
func scriptedStream(text string, err error) agentllm.StreamFunc {
|
||||
return func(_ context.Context, _ string, msgs []agentllm.Message, _ []agentllm.ToolDecl, _ agentllm.StreamOptions) <-chan agentllm.Event {
|
||||
ch := make(chan agentllm.Event, 3)
|
||||
if err != nil {
|
||||
ch <- agentllm.Event{Kind: agentllm.KindError, Err: err}
|
||||
} else {
|
||||
ch <- agentllm.Event{Kind: agentllm.KindTextDelta, Text: text}
|
||||
}
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
}
|
||||
|
||||
func catalogFixture() []agentintent.CatalogSpec {
|
||||
return []agentintent.CatalogSpec{
|
||||
{ID: "deploy.run", Description: "部署应用", Slots: []agentintent.CatalogSlot{{Key: "env", Required: true}}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightGeneratorParsesCommands(t *testing.T) {
|
||||
g := LightGenerator{
|
||||
Stream: scriptedStream(" ```json\n[{\"kind\":\"start\",\"action\":{\"kind\":\"workflow\",\"id\":\"deploy.run\"},\"slots\":{\"env\":\"prod\"}}]\n``` ", nil),
|
||||
Model: "lite",
|
||||
}
|
||||
cmds, err := g.Generate(context.Background(), "把应用部署到生产", catalogFixture())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(cmds) != 1 || cmds[0].Kind != agentintent.CommandStart {
|
||||
t.Fatalf("期望 1 条 start 命令,得到 %+v", cmds)
|
||||
}
|
||||
if cmds[0].Action.Kind != "workflow" || cmds[0].Action.ID != "deploy.run" {
|
||||
t.Fatalf("action 不符:%+v", cmds[0].Action)
|
||||
}
|
||||
if cmds[0].Slots["env"] != "prod" {
|
||||
t.Fatalf("槽位不符:%+v", cmds[0].Slots)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightGeneratorEmptyCatalogNoCall(t *testing.T) {
|
||||
called := false
|
||||
g := LightGenerator{Stream: func(_ context.Context, _ string, _ []agentllm.Message, _ []agentllm.ToolDecl, _ agentllm.StreamOptions) <-chan agentllm.Event {
|
||||
called = true
|
||||
ch := make(chan agentllm.Event)
|
||||
close(ch)
|
||||
return ch
|
||||
}}
|
||||
cmds, err := g.Generate(context.Background(), "你好", nil)
|
||||
if err != nil || cmds != nil || called {
|
||||
t.Fatalf("空目录不应调用模型:cmds=%v err=%v called=%v", cmds, err, called)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightGeneratorStreamError(t *testing.T) {
|
||||
g := LightGenerator{Stream: scriptedStream("", errors.New("dial refused"))}
|
||||
if _, err := g.Generate(context.Background(), "部署", catalogFixture()); err == nil ||
|
||||
!strings.Contains(err.Error(), "dial refused") {
|
||||
t.Fatalf("期望流错误上抛,得到 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightGeneratorBadJSON(t *testing.T) {
|
||||
g := LightGenerator{Stream: scriptedStream("我无法判断", nil)}
|
||||
if _, err := g.Generate(context.Background(), "部署", catalogFixture()); err == nil {
|
||||
t.Fatal("期望解析失败返回错误(库侧 fail-closed 回 fallback)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommandsFencedAndChatty(t *testing.T) {
|
||||
raw := "好的,命令如下:\n```json\n[{\"kind\":\"chitchat\"}]\n```\n以上。"
|
||||
cmds, err := parseCommands(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(cmds) != 1 || cmds[0].Kind != agentintent.CommandChitchat {
|
||||
t.Fatalf("期望 chitchat,得到 %+v", cmds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratorPromptContainsCatalogOnly(t *testing.T) {
|
||||
p := generatorSystemPrompt(catalogFixture())
|
||||
for _, want := range []string{"deploy.run", "部署应用", "env"} {
|
||||
if !strings.Contains(p, want) {
|
||||
t.Fatalf("提示词缺少 %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(p, "正例") || strings.Contains(p, "examples") {
|
||||
t.Fatal("提示词不应包含示例文本(注入面)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Package intentsem 慢通道实现(I2 增量三):把 agent-intent 的
|
||||
// SemanticScorer / CommandGenerator 端口接到宿主已有的向量与轻量模型
|
||||
// 能力上。两个实现都是可插拔的——装配层默认不接线(设置开关关),
|
||||
// 此时识别行为与 I1 完全一致。
|
||||
package intentsem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
core "agent-system-core"
|
||||
)
|
||||
|
||||
// EmbeddingScorer 用语义向量实现 agentintent.SemanticScorer:
|
||||
// 对 input 与每个 example 取向量,返回最大余弦相似度。
|
||||
// 任何错误都原样上抛——库侧约定 scorer 出错时识别保持 Ambiguous,
|
||||
// 不会使 Recognize 失败(fail-open 到规则层结论)。
|
||||
type EmbeddingScorer struct {
|
||||
Provider core.EmbeddingProvider
|
||||
}
|
||||
|
||||
// Score 返回 input 与 examples 的最高余弦相似度,取值 [0,1]。
|
||||
func (s EmbeddingScorer) Score(ctx context.Context, input string, examples []string) (float64, error) {
|
||||
if s.Provider == nil || len(examples) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
texts := make([]string, 0, len(examples)+1)
|
||||
texts = append(texts, input)
|
||||
texts = append(texts, examples...)
|
||||
vecs, err := s.Provider.Embed(ctx, texts)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(vecs) != len(texts) || len(vecs[0]) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
best := 0.0
|
||||
for _, v := range vecs[1:] {
|
||||
if sim := cosine(vecs[0], v); sim > best {
|
||||
best = sim
|
||||
}
|
||||
}
|
||||
return best, nil
|
||||
}
|
||||
|
||||
// cosine 计算两个等长向量的余弦相似度;任一零向量或不等长返回 0。
|
||||
func cosine(a, b []float32) float64 {
|
||||
if len(a) == 0 || len(a) != len(b) {
|
||||
return 0
|
||||
}
|
||||
var dot, na, nb float64
|
||||
for i := range a {
|
||||
fa, fb := float64(a[i]), float64(b[i])
|
||||
dot += fa * fb
|
||||
na += fa * fa
|
||||
nb += fb * fb
|
||||
}
|
||||
if na == 0 || nb == 0 {
|
||||
return 0
|
||||
}
|
||||
return dot / (math.Sqrt(na) * math.Sqrt(nb))
|
||||
}
|
||||
@@ -94,6 +94,8 @@ func Schema() []core.SettingDesc {
|
||||
{Key: "runtime.llmRetries", Group: "runtime", Label: "模型重试次数", Type: core.SettingNumber, Default: 2, Effect: core.EffectNewRun, Description: "直连模型在还没有任何输出时,遇到超时、限流或服务端错误的额外重试次数。0 = 不重试。已经开始输出后不再重试"},
|
||||
{Key: "runtime.codeOrchestration", Group: "runtime", Label: "代码编排", Type: core.SettingBool, Default: false, Effect: core.EffectNewRun, Description: "允许模型用 run_tool_script 写脚本批量调用工具。默认关闭"},
|
||||
{Key: "runtime.composeFinal", Group: "runtime", Label: "终轮整理回复", Type: core.SettingBool, Default: true, Effect: core.EffectNewRun, Description: "用过工具的回合结束后,再用主模型写一段直接回答用户的最终回复"},
|
||||
{Key: "runtime.intentSemantic", Group: "runtime", Label: "意图语义裁决(慢通道)", Type: core.SettingBool, Default: false, Effect: core.EffectRestart, Description: "规则歧义时用 embedding 语义相似度裁决。需已配置 embedding 端点。默认关闭(行为与 I1 一致)"},
|
||||
{Key: "runtime.intentGenerator", Group: "runtime", Label: "意图命令生成(慢通道)", Type: core.SettingBool, Default: false, Effect: core.EffectRestart, Description: "规则与语义都未命中时用轻量模型生成意图命令(幻觉 fail-closed 回退)。默认关闭(行为与 I1 一致)"},
|
||||
{Key: "context.maxHistoryMessages", Group: "context", Label: "历史窗口条数", Type: core.SettingNumber, Default: 40, Effect: core.EffectNewRun},
|
||||
{Key: "context.maxToolResultChars", Group: "context", Label: "工具结果上限(字符)", Type: core.SettingNumber, Default: 20000, Effect: core.EffectNewRun},
|
||||
{Key: "context.userInstructions", Group: "context", Label: "个人指令", Type: core.SettingString, Default: "", Effect: core.EffectNewRun, Description: "每次对话附在上下文末尾,不改系统前缀"},
|
||||
|
||||
Reference in New Issue
Block a user