Week 1 Day 4:Agent Loop - 苏格拉底教学

💡 一句话核心:Agent不是"一问一答"的聊天机器人,而是一台能自主循环决策的机器——直到它认为任务完成,或者系统强制停止。今天是整个Week 1最重要的一天。预计4-5小时。

学习目标

  • 理解Agent Loop的完整状态机
  • 设计LoopState(iteration计数、messages历史、已调用工具)
  • 实现完整的决策循环:用户输入→LLM判断→ToolCall→执行→反馈→循环
  • 掌握三种终止条件:final answer / max iterations / error
  • 把Day 2的LLMClient和Day 3的Registry串起来

第一部分:问题驱动

🤔 问题1:LLM一次能解决所有问题吗?

引导问题:

  1. 用户问:"我的订单12345的退款状态,以及如果退款失败应该怎么处理?"
  2. LLM第一次需要做什么?(查订单状态,需要调用工具)
  3. 查完状态后,LLM还需要做什么?(根据状态判断,可能还要查政策)
  4. LLM怎么知道自己"完成了"?

答案揭示:

  • 复杂任务天然是多步骤
  • 每步之后,LLM需要"看到"上一步的结果,才能决定下一步
  • Agent Loop就是把这个多步骤过程自动化
  • Loop是Agent和ChatBot的根本区别

你应该理解:

ChatBot:User → LLM → Answer(1次交互) Agent:User → LLM → Tool? → Execute → LLM → Tool? → Execute → ... → Answer(N次交互)

🤔 问题2:Loop的状态应该包含什么?

引导问题:

  1. LLM每次被调用,需要看到什么上下文?(消息历史)
  2. 怎么防止Agent陷入无限循环?(iteration计数)
  3. 出问题后,你怎么知道Agent走了哪些步骤?(已调用工具记录)
  4. 工具执行失败时,信息怎么传回给LLM?(把错误加入messages)

答案揭示:

Loop需要管理的状态:

状态字段 类型 用途
Iteration int 当前第几轮,防无限循环
Messages []Message 完整对话历史,每次喂给LLM
ToolCallHistory []ToolCallRecord 已执行工具及结果,用于审计
StartTime time.Time 计算总耗时,超时兜底
LastError error 最后一次错误,用于终止判断

🤔 问题3:Agent什么时候应该停下来?

场景: Agent一直在调用工具,迟迟不给出最终答案。怎么办?

三种终止条件:

  1. Final Answer:LLM返回的response里没有ToolCall,说明它认为任务完成,输出就是最终答案
  2. Max Iterations:安全网。默认10次,超过就强制终止,返回错误
  3. Error:工具执行出了无法恢复的错误(比如LLM API挂掉),立即终止

反思: 为什么Max Iterations不设成100?

  • 每次LLM调用都有延迟和成本
  • 10次循环已经能处理绝大多数真实任务(查询→分析→创建工单≤5步)
  • 超过10次几乎一定是Agent陷入循环,没有进展

🤔 问题4:工具执行失败,要不要告诉LLM?

场景: Agent调用search_kb("SSO reset"),工具超时了。下一步怎么办?

两种选择:

  • 选项A:直接终止,返回用户错误
  • 选项B:把超时错误作为消息加入对话,继续让LLM决定

答案: 选项B——大多数情况下更好。

原因:LLM有自我修正能力。看到工具失败后,它可能:

  1. 换另一个工具尝试
  2. 主动告诉用户"我无法查到相关信息,请联系人工"
  3. 用已有的上下文给出一个低置信度的回答

规则: 工具执行错误 → 把错误信息加入messages → 继续下一轮,除非是系统级严重错误(LLM API彻底不可用)。


第二部分:状态转换图

┌─────────────────────────────┐ │ LOOP START │ │ iteration=0, messages=[user]│ └──────────────┬──────────────┘ │ ▼ ┌─────────────────────────────┐ │ CHECK ITERATION │ │ iteration >= maxIterations? │ └──────────────┬──────────────┘ YES │ NO │ ┌─────────────────────────────┐ ┌──────────┘ │ CALL LLM │ │ │ messages → LLMClient │ ▼ │ → response │ ┌─────────────┐ └──────────────┬──────────────┘ │ MAX_ITER │ error│ success │ ERROR │ ┌──────────┘ └─────────────┘ │ ▼ ┌─────────────────────────┐ │ PARSE LLM RESPONSE │ │ has tool_call? │ └──────────────┬──────────┘ YES │ NO ┌─────────────────── ┘ ┌──────────────────┐ │ │ FINAL ANSWER │ ▼ │ return result │ ┌───────────────────────┐ └──────────────────┘ │ EXECUTE TOOL │ │ registry.Execute() │ └───────────┬───────────┘ error │ success ┌──────────┘ │ ▼ ┌─────────────────────────────┐ │ APPEND RESULT TO MESSAGES │ │ (error msg OR tool result) │ │ iteration++ │ └─────────────┬───────────────┘ │ └─────────────────► 回到 CHECK ITERATION

第三部分:动手实现

✅ 版本1:LoopState 和 Agent struct

// internal/agent/runtime.go
package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"time"

	"yourname/agent-runtime/internal/llm"
	"yourname/agent-runtime/internal/tools"
)

// ToolCallRecord 一条工具调用记录(用于审计和debug)
type ToolCallRecord struct {
	Iteration int             // 第几轮Loop
	ToolName  string          // 调用的工具名
	Arguments json.RawMessage // 原始参数
	Result    string          // 工具返回的Output
	Error     string          // 如果有错误
	Duration  time.Duration   // 执行耗时
}

// LoopState 记录一次Agent执行的完整状态
type LoopState struct {
	Iteration       int
	Messages        []llm.Message
	ToolCallHistory []ToolCallRecord
	StartTime       time.Time
	LastError       error
}

// AgentResponse Agent执行完毕的结果
type AgentResponse struct {
	Answer          string           // 最终答案(给用户)
	TotalIterations int              // 总共循环了几次
	ToolCallHistory []ToolCallRecord // 调用链(给审计系统)
	Duration        time.Duration    // 总耗时
}

// Runtime Agent的核心运行时
type Runtime struct {
	llmClient     llm.LLMClient
	toolRegistry  *tools.Registry
	maxIterations int
	systemPrompt  string
}

// NewRuntime 创建Agent运行时
func NewRuntime(client llm.LLMClient, registry *tools.Registry, opts ...Option) *Runtime {
	r := &Runtime{
		llmClient:     client,
		toolRegistry:  registry,
		maxIterations: 10, // 安全默认值
		systemPrompt:  defaultSystemPrompt,
	}
	for _, o := range opts {
		o(r)
	}
	return r
}

// Option 功能选项(对外扩展点)
type Option func(*Runtime)

func WithMaxIterations(n int) Option {
	return func(r *Runtime) { r.maxIterations = n }
}

func WithSystemPrompt(prompt string) Option {
	return func(r *Runtime) { r.systemPrompt = prompt }
}

const defaultSystemPrompt = `You are a helpful customer support agent.
You have access to tools to look up information and create tickets.
Use tools when you need real information. When you have a complete answer, respond directly without calling any tool.`

✅ 版本2:核心 Run 方法

// internal/agent/runtime.go(续)

// Run 执行Agent Loop
// 这是整个系统最核心的方法,务必彻底理解每一行
func (r *Runtime) Run(ctx context.Context, userInput string) (*AgentResponse, error) {
	// 初始化 Loop State
	state := &LoopState{
		StartTime: time.Now(),
		Messages: []llm.Message{
			{Role: "system", Content: r.systemPrompt},
			{Role: "user", Content: userInput},
		},
	}

	slog.Info("agent.run.start",
		"user_input", userInput,
		"max_iterations", r.maxIterations,
	)

	// === 主循环 ===
	for state.Iteration < r.maxIterations {
		slog.Info("agent.loop.iteration",
			"iteration", state.Iteration,
			"message_count", len(state.Messages),
		)

		// Step 1:调用LLM
		llmResp, err := r.callLLM(ctx, state)
		if err != nil {
			// LLM调用本身失败(网络、认证等),不可恢复,直接终止
			return nil, fmt.Errorf("llm call failed at iteration %d: %w", state.Iteration, err)
		}

		// Step 2:检查是否有 ToolCall
		if len(llmResp.ToolCalls) == 0 {
			// 没有工具调用 → 这是最终答案
			slog.Info("agent.run.finished",
				"iterations", state.Iteration+1,
				"duration_ms", time.Since(state.StartTime).Milliseconds(),
			)
			return &AgentResponse{
				Answer:          llmResp.Content,
				TotalIterations: state.Iteration + 1,
				ToolCallHistory: state.ToolCallHistory,
				Duration:        time.Since(state.StartTime),
			}, nil
		}

		// Step 3:把LLM的助手消息加入历史
		// 注意:OpenAI规范要求 tool_calls 这轮的 assistant 消息先加入
		state.Messages = append(state.Messages, llm.Message{
			Role:      "assistant",
			Content:   llmResp.Content,
			ToolCalls: llmResp.ToolCalls,
		})

		// Step 4:执行所有 ToolCalls(可能有多个)
		r.processToolCalls(ctx, state, llmResp.ToolCalls)

		// Step 5:iteration计数
		state.Iteration++
	}

	// 超过最大迭代次数
	slog.Warn("agent.run.max_iterations_exceeded",
		"max", r.maxIterations,
		"duration_ms", time.Since(state.StartTime).Milliseconds(),
	)
	return nil, fmt.Errorf("agent exceeded max iterations (%d)", r.maxIterations)
}

✅ 版本3:callLLM 辅助方法

// internal/agent/runtime.go(续)

// callLLM 封装LLM调用,把当前state转换成请求格式
func (r *Runtime) callLLM(ctx context.Context, state *LoopState) (*llm.GenerateResponse, error) {
	// 把registry的工具格式化给LLM
	openaiTools := r.toolRegistry.ToOpenAIFormat()

	req := llm.GenerateRequest{
		Messages:  state.Messages,
		Tools:     openaiTools,
		// 当有工具可用时,让LLM自己决定是否调用工具
		ToolChoice: "auto",
	}

	start := time.Now()
	resp, err := r.llmClient.Generate(ctx, req)
	duration := time.Since(start)

	if err != nil {
		slog.Error("agent.llm.error",
			"iteration", state.Iteration,
			"duration_ms", duration.Milliseconds(),
			"error", err,
		)
		return nil, err
	}

	slog.Info("agent.llm.ok",
		"iteration", state.Iteration,
		"duration_ms", duration.Milliseconds(),
		"tool_calls_count", len(resp.ToolCalls),
		"has_content", resp.Content != "",
	)
	return resp, nil
}

✅ 版本4:processToolCalls - 执行工具并把结果喂回

// internal/agent/runtime.go(续)

// processToolCalls 执行所有tool_calls,把结果作为tool消息加入对话
// 每个工具调用都独立执行,互不影响
func (r *Runtime) processToolCalls(ctx context.Context, state *LoopState, calls []llm.ToolCall) {
	for _, call := range calls {
		record := ToolCallRecord{
			Iteration: state.Iteration,
			ToolName:  call.Name,
			Arguments: call.Arguments,
		}
		start := time.Now()

		slog.Info("agent.tool.execute",
			"iteration", state.Iteration,
			"tool", call.Name,
			"args", string(call.Arguments),
		)

		// 执行工具(Registry.Execute内部已经有timeout和panic recovery)
		toolResult := r.toolRegistry.Execute(ctx, call.Name, call.Arguments)

		record.Duration = time.Since(start)

		// 构造要加入对话的消息内容
		var messageContent string
		if toolResult.Error != "" {
			// 工具执行失败:把错误告诉LLM,让它决定下一步
			messageContent = fmt.Sprintf("Tool execution failed: %s", toolResult.Error)
			record.Error = toolResult.Error
			slog.Warn("agent.tool.failed",
				"iteration", state.Iteration,
				"tool", call.Name,
				"error", toolResult.Error,
				"duration_ms", record.Duration.Milliseconds(),
			)
		} else {
			// 工具执行成功
			messageContent = toolResult.Output
			record.Result = toolResult.Output
			slog.Info("agent.tool.success",
				"iteration", state.Iteration,
				"tool", call.Name,
				"output_len", len(toolResult.Output),
				"duration_ms", record.Duration.Milliseconds(),
			)
		}

		// 把工具结果加入消息历史
		// OpenAI的规范:role=tool,需要带 tool_call_id
		state.Messages = append(state.Messages, llm.Message{
			Role:       "tool",
			Content:    messageContent,
			ToolCallID: call.ID, // 对应assistant消息里的call.id
		})

		// 记录到审计历史
		state.ToolCallHistory = append(state.ToolCallHistory, record)
	}
}

✅ 版本5:LLM类型扩展(配合Day 2的client)

Day 2实现的LLMClient需要支持ToolCalls,补充消息类型:

// internal/llm/types.go(在Day 2基础上扩展)
package llm

import "encoding/json"

// Message 对话消息(扩展版)
type Message struct {
	Role       string     `json:"role"`
	Content    string     `json:"content"`
	ToolCalls  []ToolCall `json:"tool_calls,omitempty"`  // assistant发出的工具调用
	ToolCallID string     `json:"tool_call_id,omitempty"` // tool消息回传时的ID
}

// ToolCall LLM发出的工具调用请求
type ToolCall struct {
	ID        string          `json:"id"`       // OpenAI分配的唯一ID
	Name      string          `json:"name"`     // 工具名称
	Arguments json.RawMessage `json:"arguments"` // JSON格式的参数
}

// GenerateRequest 发给LLM的请求(扩展版)
type GenerateRequest struct {
	Messages   []Message        `json:"messages"`
	Tools      []map[string]any `json:"tools,omitempty"`
	ToolChoice string           `json:"tool_choice,omitempty"` // "auto" | "none" | "required"
	RequestID  string           `json:"-"`
	Model      string           `json:"-"`
}

// GenerateResponse LLM返回的响应
type GenerateResponse struct {
	Content   string     // 文本内容(如果是final answer)
	ToolCalls []ToolCall // 工具调用列表(如果LLM决定调用工具)
}

✅ 版本6:把所有东西接入HTTP Handler

// internal/server/handler.go(更新版)
package server

import (
	"encoding/json"
	"log/slog"
	"net/http"

	"yourname/agent-runtime/internal/agent"
)

type ChatRequest struct {
	Message   string `json:"message"`
	RequestID string `json:"request_id,omitempty"`
}

type ChatResponse struct {
	Answer          string                   `json:"answer"`
	TotalIterations int                      `json:"total_iterations"`
	ToolCallHistory []agent.ToolCallRecord   `json:"tool_call_history,omitempty"`
	DurationMs      int64                    `json:"duration_ms"`
}

func (s *Server) ChatHandler(w http.ResponseWriter, r *http.Request) {
	var req ChatRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}
	if req.Message == "" {
		http.Error(w, "message is required", http.StatusBadRequest)
		return
	}

	// 执行Agent Loop
	agentResp, err := s.agentRuntime.Run(r.Context(), req.Message)
	if err != nil {
		slog.Error("agent.run.error", "error", err, "request_id", req.RequestID)
		http.Error(w, "agent failed: "+err.Error(), http.StatusInternalServerError)
		return
	}

	resp := ChatResponse{
		Answer:          agentResp.Answer,
		TotalIterations: agentResp.TotalIterations,
		ToolCallHistory: agentResp.ToolCallHistory,
		DurationMs:      agentResp.Duration.Milliseconds(),
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

✅ 版本7:完整组装(main.go 片段)

// cmd/api/main.go(核心部分)
package main

import (
	"yourname/agent-runtime/internal/agent"
	"yourname/agent-runtime/internal/llm"
	"yourname/agent-runtime/internal/tools"
	"yourname/agent-runtime/internal/tools/builtin"
)

func setupAgentRuntime(cfg *config.Config) *agent.Runtime {
	// 1. LLM Client(Day 2实现)
	llmClient := llm.NewOpenAIClient(cfg.OpenAIKey, cfg.ModelName)

	// 2. Tool Registry(Day 3实现)
	registry := tools.NewRegistry()
	_ = registry.Register(builtin.NewSearchKB())
	_ = registry.Register(builtin.NewCreateTicket())
	_ = registry.Register(builtin.NewGetUserInfo())

	// 3. Agent Runtime(Day 4实现)
	runtime := agent.NewRuntime(
		llmClient,
		registry,
		agent.WithMaxIterations(10),
	)

	return runtime
}

第四部分:关键概念深挖

1. 为什么Messages历史必须完整保留?

iteration 0: messages: [system, user] → LLM返回: tool_call(search_kb, "SSO reset") iteration 1: messages: [system, user, assistant(tool_calls), tool(search_kb result)] → LLM看到了"search_kb"的结果,才能决定下一步 iteration 2: messages: [system, user, assistant(tool_calls), tool(search_kb result), assistant(tool_calls), tool(create_ticket result)] → LLM看到了完整的操作历史

如果你截断了历史,LLM会"失忆"——不知道已经查过什么,可能重复调用工具。


2. OpenAI的消息格式约束

OpenAI对消息顺序有严格要求,违反会报API错误:

正确顺序: [system] → [user] → [assistant with tool_calls] → [tool] → [assistant] → ... 错误(tool消息必须在assistant之后): [system] → [user] → [tool] → ... ← 会报错!

代码里processToolCalls之前必须先append(state.Messages, assistant消息),顺序不能错。


3. 一次response里有多个ToolCall

LLM可以在一次response里要求调用多个工具

{
  "tool_calls": [
    {"id": "call_1", "name": "search_kb", "arguments": "{\"query\": \"SSO\"}"},
    {"id": "call_2", "name": "get_user_info", "arguments": "{\"user_id\": \"u_001\"}"}
  ]
}

processToolCallsfor range处理多个。每个工具调用都需要一条对应的tool消息回传(带相同的tool_call_id)。


4. Max Iterations = 安全气囊,不是目标

// 不要误解:iteration=10不是"好的Agent"的标志
// 好的Agent应该在3-5轮内完成任务
// iteration=10说明任务很复杂或者Agent陷入了问题

// 生产环境建议:监控平均iteration次数
// 如果平均>5,说明prompt或工具设计有问题

5. Context传播

// Run的ctx必须贯穿所有子调用
// 如果用户HTTP请求超时,ctx会被cancel
// callLLM里的LLM调用会中断
// processToolCalls里的工具调用也会中断
// Agent不会继续循环浪费资源

func (r *Runtime) Run(ctx context.Context, ...) {
    for {
        llmResp, err := r.callLLM(ctx, state)   // ctx传入
        r.processToolCalls(ctx, state, ...)        // ctx传入
    }
}

第五部分:自测清单

运行前,问自己:

  • 我能手画 Loop 的状态转换图,标出所有入口和出口
  • 为什么 iteration 从 0 开始,iteration < maxIterations 而不是 <=
  • processToolCalls 执行前,为什么要先把 assistant 消息加入 state.Messages?
  • 工具返回错误时,Loop 会终止吗?(不会,错误会被喂给LLM)
  • 哪种情况下 Loop 会立即终止返回 error?(LLM API调用失败,或达到max_iterations)
  • 如果 LLM 在同一轮里要求调用 3 个工具,processToolCalls 会产生几条 messages?(3条)
  • 我能解释为什么 messages 历史不能被截断?

第六部分:作业

任务1:完整实现

  • internal/agent/runtime.go —— LoopState、Runtime、Run、callLLM、processToolCalls
  • internal/llm/types.go —— 扩展Message支持ToolCalls
  • internal/server/handler.go —— 更新ChatHandler使用agentRuntime

任务2:手动测试三种场景

# 启动服务
go run cmd/api/main.go

# 场景1:直接回答(无工具调用,iteration=1)
curl -X POST http://localhost:8080/chat \
  -H "Content-Type: application/json" \
  -d '{"message":"你好,请问你是谁?"}'

# 场景2:需要工具(查知识库)
curl -X POST http://localhost:8080/chat \
  -H "Content-Type: application/json" \
  -d '{"message":"如何重置SSO密码?"}'

# 场景3:多步骤(查用户 + 可能创建工单)
curl -X POST http://localhost:8080/chat \
  -H "Content-Type: application/json" \
  -d '{"message":"查询用户u_001的信息,然后帮他创建一个登录问题的工单"}'

任务3:加入 iteration 监控日志

  • 每次Loop结束,输出 total_iterationstotal_tool_callsduration_ms
  • 如果 total_iterations >= maxIterations/2,输出 warning

任务4:单元测试

// internal/agent/runtime_test.go
func TestRun_DirectAnswer_NoToolCalls(t *testing.T) {
    // mock LLMClient 返回没有tool_calls的response
    // 验证:iteration=1,answer非空,ToolCallHistory为空
}

func TestRun_SingleToolCall_ThenAnswer(t *testing.T) {
    // mock LLMClient 第1次返回tool_call,第2次返回final answer
    // 验证:iteration=2,ToolCallHistory长度=1
}

func TestRun_MaxIterationsExceeded(t *testing.T) {
    // mock LLMClient 永远返回tool_call
    // 验证:返回error,包含"max iterations"
}

func TestRun_ToolError_ContinuesLoop(t *testing.T) {
    // Registry返回tool error
    // 验证:error被加入messages,loop继续,不提前终止
}

第七部分:常见问题解答

Q1:LLM每次都选同一个工具,陷入循环怎么办?

A: 两种防御:

  1. Max Iterations(硬限制,已实现)
  2. 在LoopState里记录每个工具的调用次数,超过3次触发"循环检测"并注入提示消息:"You have called tool X 3 times. Please provide a final answer with the information you have."
// 简单实现
toolCallCount := map[string]int{}
for _, r := range state.ToolCallHistory {
    toolCallCount[r.ToolName]++
}
if toolCallCount[call.Name] >= 3 {
    state.Messages = append(state.Messages, llm.Message{
        Role:    "user",
        Content: fmt.Sprintf("Warning: You have already called '%s' %d times. Please provide your final answer now.", call.Name, toolCallCount[call.Name]),
    })
}

Q2:messages历史越来越长,会不会超出LLM的context window?

A: 会。这是真实系统的核心挑战之一(Week 3会专门处理)。

  • GPT-4o context window:128K tokens
  • 10次循环的messages一般在5K-20K tokens,大多数时候不超
  • 如果超了:需要做"history truncation"——只保留最近N条消息+系统提示
  • Day 4先不管这个,等到需要时再加

Q3:多个goroutine可以同时调用runtime.Run吗?

A: 可以,前提是:

  • LoopState是函数内的局部变量(每次调用有独立的state,不共享)
  • LLMClientGenerate方法必须是goroutine安全的(通常HTTP调用本身是安全的)
  • Registry.Execute已经有读写锁(Day 3实现)
// 这样是安全的(两个请求用同一个runtime)
go runtime.Run(ctx1, "用户A的问题")
go runtime.Run(ctx2, "用户B的问题")

Q4:如果工具调用的arguments解析失败,应该怎么处理?

A: 在Registry.Execute内部(Day 3实现),json.Unmarshal失败会返回ToolResult.Error,这个错误会被加入messages喂给LLM。LLM通常会意识到参数格式有问题并修正。

如果你想更主动,可以在processToolCalls里提前检测:

var rawCheck map[string]any
if err := json.Unmarshal(call.Arguments, &rawCheck); err != nil {
    // 直接加错误消息,跳过工具执行
    state.Messages = append(state.Messages, llm.Message{
        Role:    "tool",
        Content: "Error: invalid JSON arguments for " + call.Name,
        ToolCallID: call.ID,
    })
    continue
}

Q5:agentRuntime.Run返回的Answer应该原样返回给用户吗?

A: Day 4先原样返回。Day 5会加AgentAnswer的结构化格式和validation。 如果你现在想加简单保护:

if strings.TrimSpace(agentResp.Answer) == "" {
    agentResp.Answer = "I was unable to find a complete answer. Please try rephrasing your question."
}

配套算法题

今天的主题:链表操作。链表的节点指针操作思维对应Agent Loop里的"指向下一步"的决策链。

题1:Reverse Linked List(LeetCode 206)- Easy

// algo/reverse_linked_list.go
package algo

type ListNode struct {
    Val  int
    Next *ListNode
}

// 迭代法:O(n) / O(1)
func reverseList(head *ListNode) *ListNode {
    var prev *ListNode
    curr := head
    for curr != nil {
        next := curr.Next // 保存下一个节点(关键!否则断链)
        curr.Next = prev  // 反转指针
        prev = curr       // prev前进
        curr = next       // curr前进
    }
    return prev
}

// 递归法(理解递归思维)
func reverseListRecursive(head *ListNode) *ListNode {
    if head == nil || head.Next == nil {
        return head
    }
    // 递归到尾部
    newHead := reverseListRecursive(head.Next)
    // 反转指针
    head.Next.Next = head
    head.Next = nil
    return newHead
}

讲解: 迭代法的三指针模板(prev, curr, next)是链表操作的基石。在Agent上下文里: 就像LoopState里的iteration指针——处理完当前节点,再指向下一个,直到nil(Loop终止)。

追问: 迭代法空间O(1),递归法空间O(n)(递归栈),为什么?


题2:Linked List Cycle(LeetCode 141)- Easy

// algo/linked_list_cycle.go
package algo

// Floyd龟兔算法:O(n) / O(1)
func hasCycle(head *ListNode) bool {
    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow = slow.Next        // 慢指针:一次走1步
        fast = fast.Next.Next   // 快指针:一次走2步
        if slow == fast {        // 相遇 → 有环
            return true
        }
    }
    return false // fast走到nil → 无环
}

讲解: 龟兔算法。如果有环,快指针一定会追上慢指针。无环,快指针先到nil退出。

联系Agent: Agent Loop里检测是否陷入"工具调用环",就是用类似的思想——记录每个工具的调用次数(类比慢指针位置),如果某个工具被调了太多次就判定"有环"。

追问: 如何找到环的入口节点?(LeetCode 142——快慢指针相遇后,再从head和相遇点同步走,再次相遇即为入口)


题3:Merge Two Sorted Lists(LeetCode 21)- Easy

// algo/merge_sorted_lists.go
package algo

// 迭代法:O(m+n) / O(1)
func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
    // dummy哨兵节点:避免处理head为空的特殊情况
    dummy := &ListNode{}
    curr := dummy

    for list1 != nil && list2 != nil {
        if list1.Val <= list2.Val {
            curr.Next = list1
            list1 = list1.Next
        } else {
            curr.Next = list2
            list2 = list2.Next
        }
        curr = curr.Next
    }

    // 把剩余部分直接接上
    if list1 != nil {
        curr.Next = list1
    } else {
        curr.Next = list2
    }

    return dummy.Next
}

讲解: dummy哨兵节点是链表操作的标准技巧——避免了"如果head为空怎么办"的边界判断,代码更干净。

联系Agent: 类比processToolCalls里处理多个工具结果的合并——每个工具的输出(两条"链")需要有序地合并进messages历史。

追问: 递归版本怎么写?(递归版代码更短但空间O(m+n))


下一步:Day 5 预告

明天我们会:

  1. 把LLM的返回从"一段文字"升级为Structured Output(AgentAnswer struct)
  2. 定义confidencesourcesneeds_human_review等关键字段
  3. 实现字段间的约束校验(sources为空不能说high confidence)
  4. 用OpenAI的response_format强制LLM输出合法JSON

准备问题:

  • 如果LLM回答了"SSO密码可以在portal重置",你怎么知道它是在引用真实文档还是在编造?
  • 为什么confidence要用low/medium/high而不是0.0-1.0的浮点数?
  • 下游的工单系统想知道"Agent调用了哪些工具",你从哪里获取这个信息?