refactor: replace skill code with skill ID across the application

- Updated skill handling to use skill IDs instead of skill codes in various components, services, and models.
- Modified tests to reflect changes in skill identification.
- Removed references to skill codes in favor of skill IDs for consistency and clarity.
- Updated localization files to remove skill code references and adjust error messages accordingly.
This commit is contained in:
mlogclub
2026-06-20 20:42:07 +08:00
parent 39c648f3c1
commit 541e4c5874
49 changed files with 258 additions and 341 deletions
+5 -2
View File
@@ -16,8 +16,11 @@ var newCandidateLoader = func() *candidateLoader {
type candidateLoader struct {
}
func (l *candidateLoader) findManualSkillDefinition(skillCode string) *models.SkillDefinition {
return repositories.SkillDefinitionRepository.GetByCode(sqls.DB(), skillCode)
func (l *candidateLoader) findManualSkillDefinition(skillDefinitionID int64) *models.SkillDefinition {
if skillDefinitionID <= 0 {
return nil
}
return repositories.SkillDefinitionRepository.Get(sqls.DB(), skillDefinitionID)
}
func (l *candidateLoader) loadCandidateSkills(aiAgent models.AIAgent) []models.SkillDefinition {
+7 -11
View File
@@ -11,12 +11,11 @@ import (
func TestBuildRunLogMatchedPlan(t *testing.T) {
log := BuildRunLog(
RuntimeContext{
AIAgent: models.AIAgent{ID: 22},
AIConfig: models.AIConfig{ID: 33},
ConversationID: 11,
ManualSkillCode: "manual_refund",
IntentCode: "refund",
UserMessage: "我要退款",
AIAgent: models.AIAgent{ID: 22},
AIConfig: models.AIConfig{ID: 33},
ConversationID: 11,
ManualSkillDefinitionID: 44,
UserMessage: "我要退款",
},
&ExecutionPlan{
AIAgent: models.AIAgent{ID: 22},
@@ -25,10 +24,7 @@ func TestBuildRunLogMatchedPlan(t *testing.T) {
ModelName: "gpt-test",
Provider: enums.AIProviderOpenAI,
},
Skill: &models.SkillDefinition{
ID: 44,
Code: "refund_skill",
},
Skill: &models.SkillDefinition{ID: 44},
MatchReason: "llm_route",
},
&ExecutionTrace{Status: "ok"},
@@ -41,7 +37,7 @@ func TestBuildRunLogMatchedPlan(t *testing.T) {
if log.ConversationID != 11 || log.AIAgentID != 22 || log.AIConfigID != 33 {
t.Fatalf("unexpected ids in run log: %#v", log)
}
if !log.Matched || !log.FinalSelected || log.SkillCode != "refund_skill" {
if !log.Matched || !log.FinalSelected || log.SkillDefinitionID != 44 {
t.Fatalf("expected matched skill log, got %#v", log)
}
if log.MatchReason != "llm_route" {
+9 -23
View File
@@ -2,13 +2,10 @@ package skills
import (
"context"
"strings"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"github.com/mlogclub/simple/common/strs"
)
type intentTriggerConfig struct {
@@ -18,45 +15,34 @@ type intentTriggerConfig struct {
// MatchSkill 对单个 SkillDefinition 执行命中判断。
func MatchSkill(execCtx context.Context, ctx RuntimeContext) (*models.SkillDefinition, string, *RouteTrace, error) {
loader := newCandidateLoader()
if strs.IsNotBlank(ctx.ManualSkillCode) {
skill := loader.findManualSkillDefinition(ctx.ManualSkillCode)
if ctx.ManualSkillDefinitionID > 0 {
skill := loader.findManualSkillDefinition(ctx.ManualSkillDefinitionID)
if skill == nil || skill.Status != enums.StatusOk {
return nil, "", nil, errorsx.InvalidParamI18n("error.e0054")
}
return skill, "manual_skill_code", &RouteTrace{
Status: "manual_selected",
SelectedSkillCode: skill.Code,
return skill, "manual_skill_id", &RouteTrace{
Status: "manual_selected",
SelectedSkillID: skill.ID,
}, nil
}
candidates := loader.loadCandidateSkills(ctx.AIAgent)
trace := &RouteTrace{
Status: "started",
CandidateSkillCodes: make([]string, 0, len(candidates)),
Status: "started",
CandidateSkillIDs: make([]int64, 0, len(candidates)),
}
for _, item := range candidates {
trace.CandidateSkillCodes = append(trace.CandidateSkillCodes, item.Code)
trace.CandidateSkillIDs = append(trace.CandidateSkillIDs, item.ID)
}
if len(candidates) == 0 {
trace.Status = "no_candidate"
return nil, "no_enabled_skill_bound", trace, nil
}
intentCode := strings.TrimSpace(ctx.IntentCode)
if intentCode != "" {
for _, item := range candidates {
if strings.EqualFold(strings.TrimSpace(item.Code), intentCode) {
trace.Status = "intent_selected"
trace.SelectedSkillCode = item.Code
return &item, "intent_code", trace, nil
}
}
}
selected, routeTrace, err := routeSkillWithLLM(execCtx, ctx, candidates)
if routeTrace != nil {
trace.Status = routeTrace.Status
trace.SelectedSkillCode = routeTrace.SelectedSkillCode
trace.SelectedSkillID = routeTrace.SelectedSkillID
trace.RawDecision = routeTrace.RawDecision
trace.LatencyMs = routeTrace.LatencyMs
trace.Error = routeTrace.Error
+13 -6
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
@@ -13,10 +14,10 @@ import (
"github.com/mlogclub/simple/common/strs"
)
const routeSkillSystemPrompt = `你是客服技能路由器。你只能在候选 Skill 中选择一个最合适的 skillCode,或者返回 NONE。
const routeSkillSystemPrompt = `你是客服技能路由器。你只能在候选 Skill 中选择一个最合适的 skillId,或者返回 NONE。
只有当用户问题与 Skill 的职责边界明确匹配时才选择;
如果不明确、信息不足、多个 Skill 都不够确定,就返回 NONE。
输出只能是 skillCode 或 NONE,不能输出其他内容。`
输出只能是 skillId 或 NONE,不能输出其他内容。`
func routeSkillWithLLM(ctx context.Context, runtimeCtx RuntimeContext, candidates []models.SkillDefinition) (*models.SkillDefinition, *RouteTrace, error) {
trace := &RouteTrace{Status: "started"}
@@ -43,10 +44,16 @@ func routeSkillWithLLM(ctx context.Context, runtimeCtx RuntimeContext, candidate
trace.Status = "not_matched"
return nil, trace, nil
}
selectedID, parseErr := strconv.ParseInt(decision, 10, 64)
if parseErr != nil || selectedID <= 0 {
trace.Status = "invalid_decision"
trace.Error = fmt.Sprintf("invalid route decision: %s", decision)
return nil, trace, nil
}
for _, item := range candidates {
if strings.EqualFold(item.Code, decision) {
if item.ID == selectedID {
trace.Status = "llm_selected"
trace.SelectedSkillCode = item.Code
trace.SelectedSkillID = item.ID
return &item, trace, nil
}
}
@@ -62,14 +69,14 @@ func buildSkillRoutePrompt(userMessage string, candidates []models.SkillDefiniti
lines = append(lines, "")
lines = append(lines, "候选 Skills")
for _, item := range candidates {
line := fmt.Sprintf("- skillCode=%s; name=%s; description=%s", strings.TrimSpace(item.Code), strings.TrimSpace(item.Name), strings.TrimSpace(item.Description))
line := fmt.Sprintf("- skillId=%d; name=%s; description=%s", item.ID, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description))
if examples := parseSkillExamples(item.Examples); len(examples) > 0 {
line += "; examples=" + strings.Join(examples, " | ")
}
lines = append(lines, line)
}
lines = append(lines, "")
lines = append(lines, "请只输出一个 skillCode 或 NONE。")
lines = append(lines, "请只输出一个 skillId 或 NONE。")
return strings.Join(lines, "\n")
}
+5 -5
View File
@@ -18,7 +18,7 @@ func TestParseSkillExamples(t *testing.T) {
}
func TestNormalizeRouteDecision(t *testing.T) {
if got := normalizeRouteDecision("```refund_skill```\n补充说明"); got != "refund_skill" {
if got := normalizeRouteDecision("```44```\n补充说明"); got != "44" {
t.Fatalf("unexpected normalized decision: %q", got)
}
if got := normalizeRouteDecision(" none "); got != "NONE" {
@@ -29,20 +29,20 @@ func TestNormalizeRouteDecision(t *testing.T) {
func TestBuildSkillRoutePrompt(t *testing.T) {
prompt := buildSkillRoutePrompt("我要申请退款", []models.SkillDefinition{
{
Code: "refund_skill",
ID: 44,
Name: "退款处理",
Description: "负责退款和退货相关问题",
Examples: `["退款进度","退货运费"]`,
},
})
if !strings.Contains(prompt, "skillCode=refund_skill") {
t.Fatalf("expected prompt to include skill code, got %q", prompt)
if !strings.Contains(prompt, "skillId=44") {
t.Fatalf("expected prompt to include skill id, got %q", prompt)
}
if !strings.Contains(prompt, "examples=退款进度 | 退货运费") {
t.Fatalf("expected prompt to include examples, got %q", prompt)
}
if !strings.Contains(prompt, "请只输出一个 skillCode 或 NONE。") {
if !strings.Contains(prompt, "请只输出一个 skillId 或 NONE。") {
t.Fatalf("expected prompt to include output constraint, got %q", prompt)
}
}
+6 -8
View File
@@ -19,13 +19,12 @@ type RunLogService struct{}
// Build 根据执行计划与运行结果构建 Skill 运行日志。
func (s *RunLogService) Build(ctx RuntimeContext, plan *ExecutionPlan, trace *ExecutionTrace, err error) *models.SkillRunLog {
log := &models.SkillRunLog{
ConversationID: ctx.ConversationID,
AIAgentID: ctx.AIAgent.ID,
ManualSkillCode: ctx.ManualSkillCode,
IntentCode: ctx.IntentCode,
UserMessage: ctx.UserMessage,
TraceData: s.buildTraceData(trace),
CreatedAt: time.Now(),
ConversationID: ctx.ConversationID,
AIAgentID: ctx.AIAgent.ID,
ManualSkillID: ctx.ManualSkillDefinitionID,
UserMessage: ctx.UserMessage,
TraceData: s.buildTraceData(trace),
CreatedAt: time.Now(),
}
if plan != nil {
log.AIConfigID = plan.AIConfig.ID
@@ -34,7 +33,6 @@ func (s *RunLogService) Build(ctx RuntimeContext, plan *ExecutionPlan, trace *Ex
if plan.Skill != nil {
log.SkillDefinitionID = plan.Skill.ID
log.SkillCode = plan.Skill.Code
log.Matched = true
log.FinalSelected = true
log.MatchReason = plan.MatchReason
+11 -12
View File
@@ -4,12 +4,11 @@ import "agent-desk/internal/models"
// RuntimeContext 表示一次 Skill 运行的输入上下文。
type RuntimeContext struct {
AIAgent models.AIAgent // AIAgent 为当前请求所属的 AI Agent,必填。
AIConfig models.AIConfig // AIConfig 为当前请求实际使用的模型配置,必填。
UserMessage string // UserMessage 为当前用户输入。
ConversationID int64 // ConversationID 为当前会话 ID,无会话上下文时为 0。
ManualSkillCode string // ManualSkillCode 为显式指定的 Skill 编码
IntentCode string // IntentCode 为上游识别出的意图编码。
AIAgent models.AIAgent // AIAgent 为当前请求所属的 AI Agent,必填。
AIConfig models.AIConfig // AIConfig 为当前请求实际使用的模型配置,必填。
UserMessage string // UserMessage 为当前用户输入。
ConversationID int64 // ConversationID 为当前会话 ID,无会话上下文时为 0。
ManualSkillDefinitionID int64 // ManualSkillDefinitionID 为显式指定的 Skill 定义ID
}
// ExecutionPlan 表示 Skill Runtime 计算出的最终路由结果。
@@ -35,12 +34,12 @@ type ExecutionTrace struct {
}
type RouteTrace struct {
Status string `json:"status"`
CandidateSkillCodes []string `json:"candidateSkillCodes,omitempty"`
SelectedSkillCode string `json:"selectedSkillCode,omitempty"`
RawDecision string `json:"rawDecision,omitempty"`
LatencyMs int64 `json:"latencyMs,omitempty"`
Error string `json:"error,omitempty"`
Status string `json:"status"`
CandidateSkillIDs []int64 `json:"candidateSkillIds,omitempty"`
SelectedSkillID int64 `json:"selectedSkillId,omitempty"`
RawDecision string `json:"rawDecision,omitempty"`
LatencyMs int64 `json:"latencyMs,omitempty"`
Error string `json:"error,omitempty"`
}
type PromptTrace struct {