feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -67,6 +68,26 @@ func parseSpecialists(cfg map[string]any) []specialistSpec {
|
||||
return out
|
||||
}
|
||||
|
||||
// funcNameSafe 匹配 OpenAI 兼容 API 对 tools[].function.name 的约束:^[a-zA-Z0-9_-]+$。
|
||||
var funcNameSafe = regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
|
||||
|
||||
// toolFuncName 把专家名规范化成合法的函数名(给模型看)。
|
||||
// 关键:专家名常是中文("条款专家"),直接当函数名会被 OpenAI 兼容 API 400 拒绝
|
||||
// (Invalid 'tools[0].function.name': does not match '^[a-zA-Z0-9_-]+$'),整个多智能体协调直接挂。
|
||||
// 非法字符 → 下划线;清空则退化为 expert_N;used 保证同一批内唯一。展示名/轨迹仍用原名。
|
||||
// 模型靠 Desc(spec.Use) 判断何时调用它,函数名本身不承载语义,故退化命名不影响派发质量。
|
||||
func toolFuncName(name string, idx int, used map[string]bool) string {
|
||||
s := strings.Trim(funcNameSafe.ReplaceAllString(name, "_"), "_-")
|
||||
if s == "" {
|
||||
s = fmt.Sprintf("expert_%d", idx+1)
|
||||
}
|
||||
if used[s] {
|
||||
s = fmt.Sprintf("%s_%d", s, idx+1)
|
||||
}
|
||||
used[s] = true
|
||||
return s
|
||||
}
|
||||
|
||||
// specialistTool 把一个专家(react.Agent 或 ChatModel)包成 Eino InvokableTool(agent-as-tool):
|
||||
// 入参 brief = lead 写给它的定制简报;返回专家的精炼结论。每次派发落一条 agent 轨迹。
|
||||
type specialistTool struct {
|
||||
@@ -130,7 +151,8 @@ func (o *Orchestrator) buildSpecialists(ctx context.Context, specs []specialistS
|
||||
}
|
||||
}
|
||||
var out []tool.BaseTool
|
||||
for _, spec := range specs {
|
||||
usedFuncNames := map[string]bool{}
|
||||
for i, spec := range specs {
|
||||
sys := firstNonEmpty(spec.System, defaultAgentSystem) + specialistCondensedSuffix
|
||||
run := o.specialistRunner(ctx, spec, sys, byName)
|
||||
if run == nil {
|
||||
@@ -140,7 +162,7 @@ func (o *Orchestrator) buildSpecialists(ctx context.Context, specs []specialistS
|
||||
out = append(out, &specialistTool{
|
||||
name: spec.Name, tr: tr, run: run, timeout: specialistTimeout,
|
||||
info: &schema.ToolInfo{
|
||||
Name: spec.Name,
|
||||
Name: toolFuncName(spec.Name, i, usedFuncNames), // 给模型的函数名须 ASCII;展示/轨迹仍用 spec.Name
|
||||
Desc: firstNonEmpty(spec.Use, "专家 "+spec.Name),
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"brief": {Type: schema.String, Desc: "给该专家的明确简报:子任务、期望输出、边界约束", Required: true},
|
||||
|
||||
@@ -89,3 +89,27 @@ func TestRunCoordinatorDegrade(t *testing.T) {
|
||||
t.Fatalf("ToolCallingModel 缺失应降级 runAgent 出答案,got %q", b.answer)
|
||||
}
|
||||
}
|
||||
|
||||
// 专家名常是中文,直接当 LLM 函数名会被 OpenAI 兼容 API 400 拒绝(^[a-zA-Z0-9_-]+$),
|
||||
// 整个多智能体协调会挂。toolFuncName 负责规范化 + 保证唯一。
|
||||
func TestToolFuncName(t *testing.T) {
|
||||
used := map[string]bool{}
|
||||
if got := toolFuncName("条款专家", 0, used); got != "expert_1" {
|
||||
t.Errorf("纯中文名应退化为 expert_1,got %q", got)
|
||||
}
|
||||
if got := toolFuncName("clause_expert", 1, used); got != "clause_expert" {
|
||||
t.Errorf("合法 ASCII 名应原样保留,got %q", got)
|
||||
}
|
||||
if got := toolFuncName("法务expert", 2, used); got != "expert" {
|
||||
t.Errorf("混合名应剥出 ASCII 部分,got %q", got)
|
||||
}
|
||||
// 与上一个 "expert" 撞名 → 加序号保证唯一
|
||||
if got := toolFuncName("风控expert", 3, used); got != "expert_4" {
|
||||
t.Errorf("撞名应加序号,got %q", got)
|
||||
}
|
||||
for _, n := range []string{"expert_1", "clause_expert", "expert", "expert_4"} {
|
||||
if !used[n] {
|
||||
t.Errorf("%q 应已登记进 used", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user