Files
Agent-System-Backend/internal/intentsem/generator.go
weichen 45b011af9f
ci / vet-and-test (pull_request) Failing after 34s
Wire the intent slow lane behind settings flags (I2 WP3)
Why: I2 increments 1-2 shipped the agent-intent ports (SemanticScorer,
CommandGenerator); the backend still had no implementation, so the slow
lane could not be enabled in the product.
Evidence: go test ./... green; lint-boundaries ok; the two new
runtime.intentSemantic / runtime.intentGenerator flags are registered
with default false and the gate defaults to the I1 recognizer when both
are off (TestIntentSlowLaneFlagsDefaultOff, TestGate*).
Impact: no behavior change by default; enabling runtime.intentSemantic
routes ambiguous rule matches through embedding cosine scoring, and
runtime.intentGenerator lets the lightweight model propose commands on
fallback with library-side fail-closed validation. Nested agent-intent
pin moves to 343b1e6 (I2 increments 1-2).
Refs: I2 increment 3
2026-10-07 11:20:41 +00:00

130 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}