后端:
1.execute 上下文瘦身第一版落地(固定 4 消息骨架 + ReAct 窗口压缩 + JSON 输出约束)
- 新建 prompt/execute_context.go:
execute 阶段改为 message[0..3] 固定结构;
加入历史摘要、当轮 ReAct 绑定展示、同工具 observation 压缩(保留最新)与工具简表返回示例提示
- 更新 prompt/execute.go:
重写 plan/ReAct 执行提示词;
补齐“可做/不可做”约束;
统一严格 JSON 指令;
补充 tool_call.arguments/abort/speak 非空等格式护栏
- 更新 model/execute_contract.go:
新增 ExecuteDecision/ToolCallIntent 自定义 Unmarshal;
兼容空字符串占位与 tool_call.parameters→arguments 回退解析
- 更新 node/correction.go:
为 correction 注入 history kind 标记,避免被当作真实用户输入污染摘要
- 更新 node/execute.go:
补齐 continue/ask_user/confirm 的 speak 兜底;
移除工具结果写入前 3000 字截断
2.工具层微调语义重构(任务视角概览 + 首个空位查询 + 移动权限收紧)
- 更新 tools/read_tools.go:
get_overview 改为任务视角全量输出(课程仅占位统计);
新增 find_first_free(首个命中位 + 当日负载明细);
find_free 保留兼容别名;
list_tasks 增加 status/category 校验与空结果纠偏文案
- 更新 tools/registry.go:
注册 find_first_free;
find_free 改兼容别名;
同步 get_overview/list_tasks/move/batch_move 描述语义
- 更新 tools/write_tools.go:
move/batch_move 仅允许 suggested,existing/pending 明确拒绝并返回可读错误
- 更新 tools/SCHEDULE_TOOLS.md:
同步 get_overview/find_first_free/list_tasks/move/batch_move 的最新入参与返回示例
- 更新 prompt/plan.go:
读工具示例由 find_free 调整为 find_first_free
3.交接文档与阶段说明同步
- 更新 newAgent/HANDOFF_粗排修复与Prompt重构.md:
更新为 2026-04-08;
补充“最新增量交接”章节(当前主矛盾、P0/P1、验证清单)
- 更新 newAgent/阶段3_上下文瘦身设计.md:
同步 existing/suggested 的 move/batch_move 约束口径
- 更新 newAgent/Log.txt:
追加本轮 execute 调试日志快照
前端:无
仓库:无
367 lines
12 KiB
Go
367 lines
12 KiB
Go
package model
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"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 string `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
|
||
d.GoalCheck = raw.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
|
||
}
|
||
|
||
// 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"`
|
||
}
|