Agent 人机协作闭环:AI 做事,人做决策,别把 Loop 写成死循环

一、Agent 替你写完了代码,然后直接 git push 到了 master

自动化的诱惑让工程师不断给 Agent 开放权限。
先是读取文件,然后是修改文件。
接着是执行命令,最后是 git push。
每一步都合理,直到某天 Agent 执行了 rm -rf /tmp/* 而路径变量恰好为空。

这不是虚构场景。
AutoGPT 和 BabyAGI 等自主 Agent 的早期用户,都经历过类似事故。
Agent 的 Loop 机制让模型可以不断调用工具、获取反馈、再调用工具。
这个循环里缺少一个关键环节——人的决策。

自主 Agent 和辅助 Agent 有本质区别。
前者追求闭环,后者追求人机协作。
工程落地中,辅助 Agent 远比自主 Agent 有价值。

二、人机协作 Loop 的设计模式

Agent 的标准 Loop 是"思考-行动-观察-再思考"。
人机协作模式在关键节点插入人工确认。

flowchart TB
    A[用户输入任务] --> B[LLM 分析并生成计划]
    B --> C{计划是否需要人工确认?}
    C -->|高风险操作| D[展示计划给用户]
    D --> E{用户批准?}
    E -->|是| F[执行工具调用]
    E -->|否| G[重新规划]
    G --> B
    C -->|低风险操作| F
    F --> H[获取工具结果]
    H --> I[LLM 分析结果]
    I --> J{任务完成?}
    J -->|是| K[汇总输出给用户]
    J -->|否| L{是否超过最大轮次?}
    L -->|是| M[请求用户干预]
    L -->|否| C

三个关键的人机协作点:
第一,高风险操作前的人工确认。
第二,超过 N 轮推理后的人工介入。
第三,置信度低于阈值时的降级处理。

三、带人工确认的 Agent Loop 实现

package agent

import (
    "context"
    "fmt"
    "log"
    "time"
)

// RiskLevel 操作风险等级
type RiskLevel int

const (
    RiskRead    RiskLevel = 0  // 只读操作
    RiskWrite   RiskLevel = 1  // 数据写入
    RiskExec    RiskLevel = 2  // 命令执行
    RiskDeploy  RiskLevel = 3  // 部署操作
)

// Action 单步操作
type Action struct {
    Tool      string         `json:"tool"`
    Params    map[string]any `json:"params"`
    RiskLevel RiskLevel      `json:"risk_level"`
    Reason    string         `json:"reason"`
}

// HumanConfirmCallback 人工确认回调
type HumanConfirmCallback func(action Action) (bool, error)

// AgentLoop Agent 执行循环
type AgentLoop struct {
    llmClient    LLMClient
    toolRegistry *ToolRegistry

    maxSteps         int
    confirmThreshold RiskLevel   // 超过此等级需要人工确认
    humanConfirm     HumanConfirmCallback
    stepTimeout      time.Duration
}

// Config Agent 配置
type Config struct {
    MaxSteps         int
    ConfirmThreshold RiskLevel
    StepTimeout      time.Duration
}

// DefaultConfig 默认配置:10 步限制,写入以上需确认
func DefaultConfig() Config {
    return Config{
        MaxSteps:         10,
        ConfirmThreshold: RiskWrite,
        StepTimeout:      30 * time.Second,
    }
}

// NewAgentLoop 创建 Agent 循环
func NewAgentLoop(
    llm LLMClient,
    registry *ToolRegistry,
    confirm HumanConfirmCallback,
    cfg Config,
) *AgentLoop {
    return &AgentLoop{
        llmClient:        llm,
        toolRegistry:     registry,
        maxSteps:         cfg.MaxSteps,
        confirmThreshold: cfg.ConfirmThreshold,
        humanConfirm:     confirm,
        stepTimeout:      cfg.StepTimeout,
    }
}

// Run 执行 Agent 任务
func (a *AgentLoop) Run(ctx context.Context, task string) (string, error) {
    messages := []Message{
        {Role: "system", Content: systemPrompt()},
        {Role: "user", Content: task},
    }

    for step := 0; step < a.maxSteps; step++ {
        // 超时保护
        stepCtx, cancel := context.WithTimeout(ctx, a.stepTimeout)
        defer cancel()

        resp, err := a.llmClient.Chat(stepCtx, messages)
        if err != nil {
            return "", fmt.Errorf("第 %d 步 LLM 调用失败: %w", step+1, err)
        }

        // 模型决定不再调用工具,输出最终结果
        if resp.ToolCall == nil {
            log.Printf("[Step %d] 任务完成,输出最终结果", step+1)
            return resp.Content, nil
        }

        action := Action{
            Tool:      resp.ToolCall.Name,
            Params:    resp.ToolCall.Args,
            RiskLevel: a.assessRisk(resp.ToolCall),
            Reason:    resp.Content,
        }

        log.Printf("[Step %d] 计划执行: %s (风险: %d)",
            step+1, action.Tool, action.RiskLevel)

        // 🔑 高风险操作需要人工确认
        if action.RiskLevel >= a.confirmThreshold {
            log.Printf("[Step %d] 需要人工确认: %s", step+1, action.Tool)
            approved, err := a.humanConfirm(action)
            if err != nil {
                return "", fmt.Errorf("人工确认异常: %w", err)
            }
            if !approved {
                messages = append(messages, Message{
                    Role:    "user",
                    Content: fmt.Sprintf("操作被拒绝。请换一种方式完成任务。被拒原因:用户不同意执行 %s。", action.Tool),
                })
                continue
            }
            log.Printf("[Step %d] 人工已批准", step+1)
        }

        // 执行工具调用
        result, toolErr := a.toolRegistry.Execute(ctx, resp.ToolCall)
        if toolErr != nil {
            // 工具执行失败,告知模型
            messages = append(messages, Message{
                Role:    "tool",
                Content: fmt.Sprintf("工具 %s 执行失败: %v", action.Tool, toolErr),
            })
            continue
        }

        // 将工具结果注入对话
        messages = append(messages,
            Message{Role: "assistant", Content: resp.Content, ToolCall: resp.ToolCall},
            Message{Role: "tool", Content: result},
        )
    }

    return "", fmt.Errorf("超过最大执行步骤 (%d),任务未完成", a.maxSteps)
}

// assessRisk 评估操作风险
func (a *AgentLoop) assessRisk(tc *ToolCall) RiskLevel {
    switch tc.Name {
    case "read_file", "search", "list_directory":
        return RiskRead
    case "write_file", "update_database", "send_email":
        return RiskWrite
    case "run_command", "execute_sql":
        return RiskExec
    case "git_push", "deploy", "restart_service":
        return RiskDeploy
    default:
        return RiskRead
    }
}

func systemPrompt() string {
    return `你是一个 AI 助手。请逐步完成任务。
每一步你可以调用一个工具。
高风险操作会需要人工确认。`
}

四、人机协作的代价与适用边界

人工确认引入等待延迟。
用户可能不在电脑前,任务会卡住。
建议在确认机制中加入超时策略。
超时后降级为只读模式或暂停任务。

确认频率过高会导致"弹窗疲劳"。
用户最终会机械地点击"同意"。
需要根据历史行为动态调整确认阈值。
用户一贯批准的操作,可以降低确认等级。

不适合人机协作的场景:
批量处理成千上万条数据的任务(每步确认不可行);
用户提供明确授权脚本的自动化运维;
非交互式的后台定时任务。

五、总结

Agent 的 Loop 必须有退出机制,不能无限循环。
人机协作闭环在高风险操作前插入人工确认。
风险分级机制让确认策略可配置。
确认超时和频率控制防止弹窗疲劳。
目标是让 AI 有效率地做事,关键决策保留在人手里。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐