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一次能解决所有问题吗?
引导问题:
- 用户问:"我的订单12345的退款状态,以及如果退款失败应该怎么处理?"
- LLM第一次需要做什么?(查订单状态,需要调用工具)
- 查完状态后,LLM还需要做什么?(根据状态判断,可能还要查政策)
- LLM怎么知道自己"完成了"?
答案揭示:
- 复杂任务天然是多步骤的
- 每步之后,LLM需要"看到"上一步的结果,才能决定下一步
- Agent Loop就是把这个多步骤过程自动化
- Loop是Agent和ChatBot的根本区别
你应该理解:
ChatBot:User → LLM → Answer(1次交互)
Agent:User → LLM → Tool? → Execute → LLM → Tool? → Execute → ... → Answer(N次交互)
🤔 问题2:Loop的状态应该包含什么?
引导问题:
- LLM每次被调用,需要看到什么上下文?(消息历史)
- 怎么防止Agent陷入无限循环?(iteration计数)
- 出问题后,你怎么知道Agent走了哪些步骤?(已调用工具记录)
- 工具执行失败时,信息怎么传回给LLM?(把错误加入messages)
答案揭示:
Loop需要管理的状态:
| 状态字段 |
类型 |
用途 |
Iteration |
int |
当前第几轮,防无限循环 |
Messages |
[]Message |
完整对话历史,每次喂给LLM |
ToolCallHistory |
[]ToolCallRecord |
已执行工具及结果,用于审计 |
StartTime |
time.Time |
计算总耗时,超时兜底 |
LastError |
error |
最后一次错误,用于终止判断 |
🤔 问题3:Agent什么时候应该停下来?
场景: Agent一直在调用工具,迟迟不给出最终答案。怎么办?
三种终止条件:
- Final Answer:LLM返回的response里没有ToolCall,说明它认为任务完成,输出就是最终答案
- Max Iterations:安全网。默认10次,超过就强制终止,返回错误
- Error:工具执行出了无法恢复的错误(比如LLM API挂掉),立即终止
反思: 为什么Max Iterations不设成100?
- 每次LLM调用都有延迟和成本
- 10次循环已经能处理绝大多数真实任务(查询→分析→创建工单≤5步)
- 超过10次几乎一定是Agent陷入循环,没有进展
🤔 问题4:工具执行失败,要不要告诉LLM?
场景: Agent调用search_kb("SSO reset"),工具超时了。下一步怎么办?
两种选择:
- 选项A:直接终止,返回用户错误
- 选项B:把超时错误作为消息加入对话,继续让LLM决定
答案: 选项B——大多数情况下更好。
原因:LLM有自我修正能力。看到工具失败后,它可能:
- 换另一个工具尝试
- 主动告诉用户"我无法查到相关信息,请联系人工"
- 用已有的上下文给出一个低置信度的回答
规则: 工具执行错误 → 把错误信息加入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\"}"}
]
}
processToolCalls用for 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传入
}
}
第五部分:自测清单
运行前,问自己:
第六部分:作业
任务1:完整实现
任务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 监控日志
任务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: 两种防御:
- Max Iterations(硬限制,已实现)
- 在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,不共享)
LLMClient的Generate方法必须是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 预告
明天我们会:
- 把LLM的返回从"一段文字"升级为Structured Output(AgentAnswer struct)
- 定义
confidence、sources、needs_human_review等关键字段
- 实现字段间的约束校验(sources为空不能说high confidence)
- 用OpenAI的
response_format强制LLM输出合法JSON
准备问题:
- 如果LLM回答了"SSO密码可以在portal重置",你怎么知道它是在引用真实文档还是在编造?
- 为什么
confidence要用low/medium/high而不是0.0-1.0的浮点数?
- 下游的工单系统想知道"Agent调用了哪些工具",你从哪里获取这个信息?