Files
smartmate/backend/newAgent/model/execute_contract.go
Losita 66c06eed0a Version: 0.9.45.dev.260427
后端:
1. execute 主链路重构为“上下文工具域 + 主动优化候选闭环”——移除 order_guard,粗排后默认进入主动微调,先诊断再从后端候选中选择 move/swap,避免 LLM 自由全局乱搜
2. 工具体系升级为动态注入协议——新增 context_tools_add / remove、工具域与二级包映射、主动优化白名单;schedule / taskclass / web 工具按域按包暴露,msg0 规则包与 execute 上下文同步重写
3. analyze_health 升级为主动优化唯一裁判入口——补齐 rhythm / tightness / profile / feasibility 指标、候选扫描与复诊打分、停滞信号、forced imperfection 判定,并把连续优化状态写回运行态
4. 任务类能力并入新 Agent 执行链——新增 upsert_task_class 写工具与启动注入事务写入;任务类模型补充学科画像与整天屏蔽配置,粗排支持 excluded_days_of_week,steady 策略改为基于目标位置/单日负载/分散度/缓冲的候选打分
5. 运行态与路由补齐优化模式语义——新增 active tool domain/packs、pending context hook、active optimize only、taskclass 写入回盘快照;区分 first_full / global_reopt / local_adjust,并完善首次粗排后默认 refine 的判定

前端:
6. 助手时间线渲染细化——推理内容改为独立 reasoning block,支持与工具/状态/正文按时序交错展示,自动收口折叠,修正 confirm reject 恢复动作

仓库:
7. newAgent 文档整体迁入 docs/backend,补充主动优化执行规划与顺序约束拆解文档,删除旧调试日志文件

PS:这次科研了2天,总算是有些进展了——LLM永远只适合做选择题、判断题,不适合做开放创新题。
2026-04-27 01:09:37 +08:00

490 lines
16 KiB
Go
Raw 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 model
import (
"encoding/json"
"fmt"
"sort"
"strings"
)
// ExecuteAction 表示 execute 阶段单轮决策的动作类型。
//
// 设计原则:
// 1. LLM 只负责“申报本轮想做什么”,不直接推进状态;
// 2. 后端只围绕这些有限动作做流程校验、证据校验、安全校验;
// 3. 动作枚举保持收敛,避免 execute 节点后续再次长成“自由文本协议”。
type ExecuteAction string
const (
// ExecuteActionContinue 表示当前步骤尚未完成,需要继续本步骤的 ReAct 循环。
ExecuteActionContinue ExecuteAction = "continue"
// ExecuteActionAskUser 表示当前步骤缺少外部信息,需要中断并追问用户。
ExecuteActionAskUser ExecuteAction = "ask_user"
// ExecuteActionConfirm 表示当前步骤准备执行写操作,但必须先进入确认闸门。
ExecuteActionConfirm ExecuteAction = "confirm"
// ExecuteActionNextPlan 表示当前步骤已完成,可以推进到下一个 plan 步骤。
ExecuteActionNextPlan ExecuteAction = "next_plan"
// ExecuteActionDone 表示整个任务已完成,可以进入最终交付。
ExecuteActionDone ExecuteAction = "done"
// ExecuteActionAbort 表示本轮流程应立即终止,并进入 deliver 做正式收口。
ExecuteActionAbort ExecuteAction = "abort"
)
// ExecuteDecision 是 execute prompt 单轮产出的统一决策结构。
//
// 职责边界:
// 1. Speak 是这轮先对用户说的话,适合在真正调工具前流式吐给前端;
// 2. Action 是模型申报的“下一步动作类型”;
// 3. Reason 是给后端和日志看的简短解释,不直接等价于完成证明;
// 4. ToolCall 只是“意图”,不代表工具已经真正执行成功。
type ExecuteDecision struct {
Speak string `json:"speak,omitempty"`
Action ExecuteAction `json:"action"`
Reason string `json:"reason,omitempty"`
GoalCheck string `json:"goal_check,omitempty"`
ToolCall *ToolCallIntent `json:"tool_call,omitempty"`
Abort *AbortIntent `json:"abort,omitempty"`
}
// UnmarshalJSON 兼容执行决策里几种模型高频跑偏但语义可恢复的写法。
//
// 职责边界:
// 1. 负责把“空字符串占位字段”归一化成未填写,避免 json 反序列化阶段直接失败;
// 2. 负责把 tool_call / abort 交给各自的兼容解析逻辑,尽量保留可恢复的信息;
// 3. 不负责业务合法性校验action 与字段互斥关系仍交给 Validate 判定。
func (d *ExecuteDecision) UnmarshalJSON(data []byte) error {
type rawExecuteDecision struct {
Speak string `json:"speak,omitempty"`
Action ExecuteAction `json:"action"`
Reason string `json:"reason,omitempty"`
GoalCheck json.RawMessage `json:"goal_check,omitempty"`
ToolCall json.RawMessage `json:"tool_call,omitempty"`
Abort json.RawMessage `json:"abort,omitempty"`
}
var raw rawExecuteDecision
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
d.Speak = raw.Speak
d.Action = raw.Action
d.Reason = raw.Reason
goalCheck, err := decodeGoalCheckText(raw.GoalCheck)
if err != nil {
return fmt.Errorf("goal_check 解析失败: %w", err)
}
d.GoalCheck = goalCheck
toolCall, err := decodeOptionalJSONObject[ToolCallIntent](raw.ToolCall)
if err != nil {
return fmt.Errorf("tool_call 解析失败: %w", err)
}
d.ToolCall = toolCall
abortIntent, err := decodeOptionalJSONObject[AbortIntent](raw.Abort)
if err != nil {
return fmt.Errorf("abort 解析失败: %w", err)
}
d.Abort = abortIntent
return nil
}
// decodeGoalCheckText 兼容 goal_check 的字符串/对象写法,统一降级为字符串。
//
// 步骤化说明:
// 1. 字符串:直接使用,保持主协议不变;
// 2. 对象:按 done_when/evidence 提取并拼接为单行证据文本;
// 3. 数组或其他标量:尽量转成可读字符串,避免仅因格式漂移导致整轮失败。
func decodeGoalCheckText(raw json.RawMessage) (string, error) {
trimmed := strings.TrimSpace(string(raw))
if trimmed == "" || trimmed == "null" {
return "", nil
}
// 1. 标准写法goal_check 为字符串。
if strings.HasPrefix(trimmed, "\"") {
var text string
if err := json.Unmarshal(raw, &text); err != nil {
return "", err
}
return strings.TrimSpace(text), nil
}
// 2. 兼容写法goal_check 被模型写成对象。
if strings.HasPrefix(trimmed, "{") {
var obj map[string]any
if err := json.Unmarshal(raw, &obj); err != nil {
return "", err
}
return compactGoalCheckObject(obj), nil
}
// 3. 兜底:数组/标量场景,尽量保留可读信息。
var generic any
if err := json.Unmarshal(raw, &generic); err != nil {
return "", err
}
return strings.TrimSpace(formatGoalCheckValue(generic)), nil
}
// compactGoalCheckObject 将对象型 goal_check 压缩为可读单行文本,优先提取 done_when/evidence。
func compactGoalCheckObject(obj map[string]any) string {
if len(obj) == 0 {
return ""
}
doneWhen := strings.TrimSpace(formatGoalCheckValue(obj["done_when"]))
evidence := strings.TrimSpace(formatGoalCheckValue(obj["evidence"]))
parts := make([]string, 0, 2)
if doneWhen != "" {
parts = append(parts, "已满足 done_when"+doneWhen)
}
if evidence != "" {
parts = append(parts, "证据:"+evidence)
}
if len(parts) > 0 {
return strings.Join(parts, "")
}
// done_when/evidence 缺失时,按 key 排序拼接,保证日志稳定可读。
keys := make([]string, 0, len(obj))
for key := range obj {
keys = append(keys, key)
}
sort.Strings(keys)
fallback := make([]string, 0, len(keys))
for _, key := range keys {
text := strings.TrimSpace(formatGoalCheckValue(obj[key]))
if text == "" {
continue
}
fallback = append(fallback, key+"="+text)
}
return strings.Join(fallback, "")
}
// formatGoalCheckValue 将任意值转成单行可读文本,用于 goal_check 压缩拼接。
func formatGoalCheckValue(value any) string {
switch typed := value.(type) {
case nil:
return ""
case string:
return strings.TrimSpace(typed)
case bool:
if typed {
return "true"
}
return "false"
case []any:
parts := make([]string, 0, len(typed))
for _, item := range typed {
text := strings.TrimSpace(formatGoalCheckValue(item))
if text == "" {
continue
}
parts = append(parts, text)
}
return strings.Join(parts, "")
case map[string]any:
keys := make([]string, 0, len(typed))
for key := range typed {
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
text := strings.TrimSpace(formatGoalCheckValue(typed[key]))
if text == "" {
continue
}
parts = append(parts, key+"="+text)
}
return strings.Join(parts, "")
default:
return strings.TrimSpace(fmt.Sprintf("%v", typed))
}
}
// Normalize 统一清洗 execute 决策中的字符串字段。
func (d *ExecuteDecision) Normalize() {
if d == nil {
return
}
d.Speak = strings.TrimSpace(d.Speak)
d.Action = ExecuteAction(strings.TrimSpace(string(d.Action)))
d.Reason = strings.TrimSpace(d.Reason)
d.GoalCheck = strings.TrimSpace(d.GoalCheck)
if d.ToolCall != nil {
d.ToolCall.Normalize()
}
if d.Abort != nil {
d.Abort.Normalize()
}
}
// Validate 校验 execute 决策的最小合法性。
//
// 校验原则:
// 1. 这里只校验“协议是否自洽”,不校验工具是否真实存在,也不校验当前步骤是否真的完成;
// 2. 只允许少量动作与 tool_call 共存,避免后续 node 层收到含糊决策;
// 3. 真正的三类最小校验应放在执行层,这里只做第一道轻量门禁。
func (d *ExecuteDecision) Validate() error {
if d == nil {
return fmt.Errorf("execute decision 不能为空")
}
d.Normalize()
if d.Action == "" {
return fmt.Errorf("execute decision.action 不能为空")
}
switch d.Action {
case ExecuteActionContinue:
if d.Abort != nil {
return fmt.Errorf("continue 动作不应携带 abort")
}
if d.ToolCall != nil {
return d.ToolCall.Validate()
}
return nil
case ExecuteActionAskUser:
if d.ToolCall != nil {
return fmt.Errorf("ask_user 动作不应携带 tool_call")
}
if d.Abort != nil {
return fmt.Errorf("ask_user 动作不应携带 abort")
}
return nil
case ExecuteActionConfirm:
if d.ToolCall == nil {
return fmt.Errorf("confirm 动作必须携带待确认的 tool_call")
}
if d.Abort != nil {
return fmt.Errorf("confirm 动作不应同时携带 abort")
}
return d.ToolCall.Validate()
case ExecuteActionNextPlan, ExecuteActionDone:
if d.ToolCall != nil {
return fmt.Errorf("%s 动作不应携带 tool_call", d.Action)
}
if d.Abort != nil {
return fmt.Errorf("%s 动作不应携带 abort", d.Action)
}
return nil
case ExecuteActionAbort:
if d.ToolCall != nil {
return fmt.Errorf("abort 动作不应携带 tool_call")
}
if d.Abort == nil {
return fmt.Errorf("abort 动作必须携带 abort 字段")
}
return d.Abort.Validate()
default:
return fmt.Errorf("未知 execute action: %s", d.Action)
}
}
// AbortIntent 表示 execute 阶段声明的正式终止意图。
//
// 说明:
// 1. code 是稳定机器码,便于后续前端/埋点识别终止类型;
// 2. user_message 是最终给用户看的收口文案;
// 3. internal_reason 只用于日志排查,允许更技术化。
type AbortIntent struct {
Code string `json:"code,omitempty"`
UserMessage string `json:"user_message"`
InternalReason string `json:"internal_reason,omitempty"`
}
// Normalize 清洗终止意图中的稳定字段。
func (a *AbortIntent) Normalize() {
if a == nil {
return
}
a.Code = strings.TrimSpace(a.Code)
a.UserMessage = strings.TrimSpace(a.UserMessage)
a.InternalReason = strings.TrimSpace(a.InternalReason)
}
// Validate 校验终止意图的最小可用性。
func (a *AbortIntent) Validate() error {
if a == nil {
return fmt.Errorf("abort 不能为空")
}
a.Normalize()
if a.UserMessage == "" {
return fmt.Errorf("abort.user_message 不能为空")
}
return nil
}
// ToolCallIntent 表示 execute 阶段申报的工具调用意图。
//
// 设计目的:
// 1. 这里只描述“模型想调用什么工具、传什么参数”,不代表调用已经发生;
// 2. Arguments 暂时保留 map 结构,方便 prompt 输出原生 JSON 对象;
// 3. 是否需要 confirm 不应由模型决定,后续应由工具注册表或后端策略判定。
type ToolCallIntent struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
}
// UnmarshalJSON 兼容 tool_call 里“arguments / parameters”两种高频字段名。
//
// 职责边界:
// 1. 优先使用标准字段 arguments保持当前正式协议不变
// 2. 仅当 arguments 缺失时,回退复用 parameters兼容模型历史习惯
// 3. 不负责校验参数是否满足具体工具 schema后续仍由工具层负责。
func (t *ToolCallIntent) UnmarshalJSON(data []byte) error {
type rawToolCallIntent struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Parameters map[string]any `json:"parameters,omitempty"`
}
var raw rawToolCallIntent
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
t.Name = raw.Name
t.Arguments = raw.Arguments
if len(t.Arguments) == 0 && len(raw.Parameters) > 0 {
t.Arguments = raw.Parameters
}
return nil
}
// Normalize 清洗工具调用意图中的稳定字段。
func (t *ToolCallIntent) Normalize() {
if t == nil {
return
}
t.Name = strings.TrimSpace(t.Name)
}
// Validate 校验工具调用意图的最小合法性。
func (t *ToolCallIntent) Validate() error {
if t == nil {
return fmt.Errorf("tool_call 不能为空")
}
t.Normalize()
if t.Name == "" {
return fmt.Errorf("tool_call.name 不能为空")
}
return nil
}
// decodeOptionalJSONObject 统一兼容“可选对象字段被模型写成空字符串”的情况。
//
// 步骤说明:
// 1. 字段缺失、null、空字符串都视为“未填写”返回 nil
// 2. 只有在确实出现对象内容时,才继续反序列化为目标结构;
// 3. 若模型传入了非空字符串等不可恢复内容,显式报错,避免把脏数据静默吞掉。
func decodeOptionalJSONObject[T any](raw json.RawMessage) (*T, error) {
trimmed := strings.TrimSpace(string(raw))
if trimmed == "" || trimmed == "null" {
return nil, nil
}
if strings.HasPrefix(trimmed, "\"") {
var text string
if err := json.Unmarshal(raw, &text); err != nil {
return nil, err
}
if strings.TrimSpace(text) == "" {
return nil, nil
}
return nil, fmt.Errorf("期望对象,实际收到非空字符串")
}
var out T
if err := json.Unmarshal(raw, &out); err != nil {
return nil, err
}
return &out, nil
}
// ExecuteEvidenceSource 表示“当前步骤完成证明”来自哪里。
type ExecuteEvidenceSource string
const (
// ExecuteEvidenceSourceToolObservation 表示来自读工具或分析工具的真实 observation。
ExecuteEvidenceSourceToolObservation ExecuteEvidenceSource = "tool_observation"
// ExecuteEvidenceSourceWriteReceipt 表示来自写工具成功执行后的回执。
ExecuteEvidenceSourceWriteReceipt ExecuteEvidenceSource = "write_receipt"
// ExecuteEvidenceSourceUserReply 表示来自用户补充回答的外部事实。
ExecuteEvidenceSourceUserReply ExecuteEvidenceSource = "user_reply"
)
// ExecuteEvidenceReceipt 表示“一条可被后端认可的最小事实证据”。
//
// 职责边界:
// 1. StepIndex 用来绑定这条证据属于哪个 plan 步骤,避免旧 observation 污染新步骤;
// 2. Source / Name / Success 描述“这条证据是怎么来的、是否真的发生了”;
// 3. Summary 只用于日志、调试和交付串联,不替代原始 observation 本身;
// 4. 这里不做语义推理,只负责记录事实。
type ExecuteEvidenceReceipt struct {
StepIndex int `json:"step_index"`
Source ExecuteEvidenceSource `json:"source"`
Name string `json:"name,omitempty"`
ArgumentsDigest string `json:"arguments_digest,omitempty"`
Success bool `json:"success"`
Summary string `json:"summary,omitempty"`
}
// Normalize 清洗证据回执中的稳定字段。
func (r *ExecuteEvidenceReceipt) Normalize() {
if r == nil {
return
}
r.Source = ExecuteEvidenceSource(strings.TrimSpace(string(r.Source)))
r.Name = strings.TrimSpace(r.Name)
r.ArgumentsDigest = strings.TrimSpace(r.ArgumentsDigest)
r.Summary = strings.TrimSpace(r.Summary)
}
// Validate 校验证据回执是否具备最小可用信息。
func (r *ExecuteEvidenceReceipt) Validate() error {
if r == nil {
return fmt.Errorf("evidence receipt 不能为空")
}
r.Normalize()
if r.StepIndex < 0 {
return fmt.Errorf("evidence receipt.step_index 不能小于 0")
}
switch r.Source {
case ExecuteEvidenceSourceToolObservation, ExecuteEvidenceSourceWriteReceipt, ExecuteEvidenceSourceUserReply:
default:
return fmt.Errorf("未知 evidence source: %s", r.Source)
}
return nil
}
// ExecuteValidationResult 保存 execute 单轮的三类最小校验结果。
//
// 三类校验语义:
// 1. FlowPassed当前动作在流程上是否合法例如 done 是否允许直接发生;
// 2. EvidencePassed当前动作是否有最小事实证据支撑
// 3. SafetyPassed当前动作是否触发了安全兜底例如超轮次、重复空转、待确认未完成。
type ExecuteValidationResult struct {
FlowPassed bool `json:"flow_passed"`
FlowReason string `json:"flow_reason,omitempty"`
EvidencePassed bool `json:"evidence_passed"`
EvidenceReason string `json:"evidence_reason,omitempty"`
SafetyPassed bool `json:"safety_passed"`
SafetyReason string `json:"safety_reason,omitempty"`
}