第1周:Agent Runtime + Tools + API - 完整概览

周目标

从零开始,做出一个可以调用LLM、执行工具、返回答案的Go后端系统。

交付物: v0.1 - 工作的Agent Runtime


整体架构

Agent Runtime 系统设计

graph TB
    User["👤 User Input"]
    
    User -->|HTTP POST /chat| API["🌐 API Server<br/>port:8080"]
    
    API -->|1.Receive| Handler["📨 Chat Handler"]
    
    Handler -->|2.Send| LLM["🧠 LLM Client<br/>OpenAI API"]
    Handler -->|3.Check| Tools["🔧 Tool Registry"]
    
    LLM -->|4.Response<br/>with tool_call?| Decision{Tool Call?}
    
    Decision -->|No| Answer["✅ Generate Answer"]
    Decision -->|Yes| ToolExec["⚙️ Execute Tool"]
    
    ToolExec -->|Result| LLM
    ToolExec -->|Error| Retry["🔁 Retry<br/>Exponential Backoff"]
    Retry -->|Max Retries| Error["❌ Return Error"]
    Retry -->|Success| LLM
    
    Answer -->|5.Response<br/>AgentAnswer| User
    Error -->|5.Response<br/>Error| User
    
    API -->|Logging| Logger["📝 Structured Log<br/>slog JSON"]
    
    style User fill:#e1f5
    style API fill:#fff4e1
    style LLM fill:#e1f5ff
    style Tools fill:#e1ffe1
    style Logger fill:#ffe1f5
    style Answer fill:#e1ffe1
    style Error fill:#ffe1e1

项目结构

graph TB
    Root["agent-runtime/"]
    
    Root --> CmdDir["cmd/"]
    Root --> InternalDir["internal/"]
    Root --> DocsDir["docs/"]
    Root --> Files["go.mod, go.sum<br/>Dockerfile<br/>README.md"]
    
    CmdDir --> MainGo["api/<br/>main.go"]
    
    InternalDir --> Server["server/<br/>server.go<br/>handler.go"]
    InternalDir --> LLM["llm/<br/>openai_client.go<br/>types.go"]
    InternalDir --> Tools["tools/<br/>registry.go<br/>definitions.go"]
    InternalDir --> Agent["agent/<br/>runtime.go<br/>output.go"]
    InternalDir --> Retry["retry/<br/>backoff.go"]
    InternalDir --> Config["config/<br/>config.go"]
    InternalDir --> Middleware["middleware/<br/>logging.go"]
    
    style Root fill:#f9f9f9
    style CmdDir fill:#fff4e1
    style InternalDir fill:#e1f5ff
    style Server fill:#e1ffe1
    style LLM fill:#e1ffe1
    style Tools fill:#e1ffe1

逐日进度

📌 Day 1: Go项目骨架 ✅

你将学到:

  • Go module和项目结构
  • HTTP服务器(用chi)
  • Structured logging(slog)
  • Graceful shutdown

关键代码:

// main.go中的优雅关闭模式
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
httpServer.Shutdown(ctx)

验证清单:

  • go run cmd/api/main.go 启动无误
  • /health 返回 {"status":"ok"}
  • /chat 接收POST返回mock答案
  • 日志输出JSON格式
  • Ctrl+C优雅关闭,等待30秒

关键问题(不查资料回答):

  1. 为什么Dockerfile需要两阶段构建?
  2. Middleware执行顺序是什么?
  3. context.Background()context.WithTimeout() 的区别?

📌 Day 2: OpenAI SDK封装

你将学到:

  • 为什么要用interface而不是直接调用SDK
  • request_id和trace传播
  • Timeout的正确用法
  • 错误分类

关键代码:

// internal/llm/openai_client.go
type LLMClient interface {
	Generate(ctx context.Context, req GenerateRequest) (*GenerateResponse, error)
}

type GenerateRequest struct {
	Messages  []Message
	RequestID string  // trace用
	Model     string
}

type OpenAIClient struct {
	client *openai.Client
	model  string
}

func (c *OpenAIClient) Generate(ctx context.Context, req GenerateRequest) (*GenerateResponse, error) {
	// 重点:ctx里面已经有timeout信息
	// 所以OpenAI SDK会自动尊重这个timeout
	resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
		Messages: convertMessages(req.Messages),
	})
	// ...
}

为什么这个设计很重要:

  1. 可替换性 - 后面可以换Azure、本地模型、fallback model
  2. 测试友好 - 可以mock这个interface做unit test
  3. 错误处理 - 统一处理OpenAI的各种错误

验证清单:

  • 新建 internal/llm/openai_client.go
  • 实现 Generate 方法
  • 支持 MODEL_NAME 环境变量切换模型
  • 实现5秒超时(Day 6会加retry)
  • /chat 现在返回真实LLM回答

关键问题:

  1. 为什么要用interface而不是直接调 openai.Client
  2. ctx 如何让OpenAI SDK知道超时时间?
  3. 如果OpenAI返回 RateLimitError,你怎么处理?

📌 Day 3: Tool Registry

你将学到:

  • 如何管理一组工具(类似插件系统)
  • Tool metadata(name、description、schema)
  • Risk level和timeout的设计
  • JSON schema的处理

关键代码:

// internal/tools/registry.go
type ToolRegistry struct {
	tools map[string]*ToolDefinition
}

type ToolDefinition struct {
	Name        string                 `json:"name"`
	Description string                 `json:"description"`
	InputSchema map[string]interface{} `json:"input_schema"` // JSON schema
	RiskLevel   string                 `json:"risk_level"`   // low/medium/high
	Timeout     time.Duration          `json:"-"`
	Execute     func(ctx context.Context, input json.RawMessage) (interface{}, error)
}

// 使用示例
func (r *ToolRegistry) Register(def *ToolDefinition) {
	r.tools[def.Name] = def
}

func (r *ToolRegistry) Execute(ctx context.Context, name string, input json.RawMessage) (interface{}, error) {
	tool, ok := r.tools[name]
	if !ok {
		return nil, fmt.Errorf("tool not found: %s", name)
	}
	return tool.Execute(ctx, input)
}

3个Mock工具:

  1. search_knowledge_base - 查询KB
  2. get_ticket_status - 查票据状态
  3. create_ticket_draft - 创建票据草稿

验证清单:

  • ToolRegistry可以注册和执行工具
  • 每个工具都有JSON schema
  • Tool execution有timeout
  • 工具不存在返回合适错误

关键问题:

  1. Risk level有什么用?(Day 18会用到)
  2. 为什么用 json.RawMessage 而不是 interface{}
  3. 如果工具执行超时,会发生什么?

📌 Day 4: Agent Loop

你将学到:

  • Agent的核心循环:提示 → LLM判断 → 工具调用 → 反馈
  • Tool call的解析(从LLM output)
  • 错误情况的处理(工具不存在、超时等)
  • Max iterations防止无限循环

Agent Loop 细节流程

graph TD
    Start["🔄 Agent Loop Iteration"]
    
    Start -->|1.Build| Conv["Conversation<br/>History"]
    
    Conv -->|2.Call| LLMStep["LLM Generate"]
    
    LLMStep -->|Response| Parse["Parse Output"]
    
    Parse -->|Check| HasTool{Has ToolCall?}
    
    HasTool -->|No| Format["Format Answer"]
    Format -->|Return| End["✅ Complete"]
    
    HasTool -->|Yes| CheckTool{"Tool<br/>Exists?"}
    
    CheckTool -->|No| NotFound["❌ Tool Not Found"]
    NotFound -->|Feedback| UpdateConv1["Update Conversation<br/>with Error"]
    UpdateConv1 -->|Next Iteration| Start
    
    CheckTool -->|Yes| Execute["⚙️ Execute Tool"]
    
    Execute -->|Check| Timeout{"Timeout?"}
    
    Timeout -->|Yes| TimeoutErr["❌ Timeout Error"]
    TimeoutErr -->|Retry| Execute
    TimeoutErr -->|Max| ReturnErr["Return to LLM"]
    
    Timeout -->|No| Result["✅ Got Result"]
    
    Result -->|Feedback| UpdateConv2["Update Conversation<br/>with Result"]
    UpdateConv2 -->|Check| MaxIter{"Max<br/>Iterations?"}
    
    MaxIter -->|Yes| MaxErr["❌ Max Iterations"]
    MaxErr -->|Return Error| End
    
    MaxIter -->|No| Start
    
    style Start fill:#fff4e1
    style End fill:#e1ffe1
    style MaxErr fill:#ffe1e1
    style NotFound fill:#ffe1e1
    style TimeoutErr fill:#ffe1e1

关键代码:

// internal/agent/runtime.go
func (r *Runtime) Execute(ctx context.Context, userInput string) (*AgentResponse, error) {
	conversation := []Message{
		{Role: "user", Content: userInput},
	}

	for iteration := 0; iteration < r.maxIterations; iteration++ {
		// 1. 调LLM,让它判断是否需要工具
		response, err := r.llmClient.Generate(ctx, GenerateRequest{
			Messages:  conversation,
			RequestID: r.getRequestID(ctx),
		})
		if err != nil {
			return nil, fmt.Errorf("llm error: %w", err)
		}

		// 2. 检查是否有tool call
		if response.ToolCall == nil {
			// 没有工具调用,直接返回答案
			return &AgentResponse{
				Answer:     response.Content,
				ToolCalls:  []string{},
			}, nil
		}

		// 3. 执行工具
		toolResult, err := r.toolRegistry.Execute(ctx, response.ToolCall.Name, response.ToolCall.Input)
		if err != nil {
			// 工具调用失败,告诉LLM发生了什么
			conversation = append(conversation, Message{
				Role:    "assistant",
				Content: response.Content,
			})
			conversation = append(conversation, Message{
				Role:    "user",
				Content: fmt.Sprintf("Tool failed: %v. Try again.", err),
			})
			continue
		}

		// 4. 把结果送回LLM
		conversation = append(conversation, Message{
			Role:    "assistant",
			Content: response.Content,
		})
		conversation = append(conversation, Message{
			Role:    "user",
			Content: fmt.Sprintf("Tool result: %s", toolResult),
		})
	}

	return nil, fmt.Errorf("max iterations exceeded")
}

需要处理的错误情况:

  • Tool not found
  • Tool input parse failed
  • Tool timeout
  • Tool returned error
  • Max iteration exceeded
  • LLM error

验证清单:

  • Agent loop可以执行完整循环
  • 工具调用失败能恢复
  • 超过max iterations返回错误
  • 每步都记录日志

关键问题:

  1. 为什么要限制max iterations?(无限循环风险)
  2. 如果工具返回错误,怎么告诉LLM?
  3. Conversation列表的目的是什么?

📌 Day 5: Structured Output

你将学到:

  • 定义最终答案结构
  • Output validation和约束
  • Schema enforcement

关键代码:

// internal/agent/output.go
type AgentAnswer struct {
	Answer           string   `json:"answer"`
	Confidence       string   `json:"confidence"`        // low/medium/high
	Sources          []string `json:"sources"`           // 信息来源
	ToolCalls        []string `json:"tool_calls"`        // 调用过的工具
	NeedsHumanReview bool     `json:"needs_human_review"` // 是否需要人工审核
}

// 验证规则
func (a *AgentAnswer) Validate() error {
	if !contains([]string{"low", "medium", "high"}, a.Confidence) {
		return fmt.Errorf("invalid confidence: %s", a.Confidence)
	}

	// 规则:sources为空时,confidence不能是high
	if len(a.Sources) == 0 && a.Confidence == "high" {
		return fmt.Errorf("cannot be high confidence without sources")
	}

	// 规则:写操作必须需要human review
	if contains(a.ToolCalls, "create_ticket") && !a.NeedsHumanReview {
		return fmt.Errorf("write operations must have NeedsHumanReview=true")
	}

	return nil
}

验证清单:

  • AgentAnswer结构定义清楚
  • Validate方法实现所有规则
  • /chat 返回结构化答案
  • 测试各种违规情况

关键问题:

  1. 为什么confidence必须是low/medium/high而不是float?
  2. 为什么写操作必须NeedsHumanReview=true?(Day 17会展开)
  3. 如何验证sources的真实性?

📌 Day 6: Retry、Backoff、Idempotency

你将学到:

  • Exponential backoff的实现
  • Idempotency key防止重复
  • Context timeout和retry的协调
  • 何时应该retry

关键代码:

// internal/retry/retry.go
type Config struct {
	MaxRetries int
	InitialBackoff time.Duration
	MaxBackoff     time.Duration
}

func Do(ctx context.Context, cfg Config, fn func() error) error {
	backoff := cfg.InitialBackoff

	for attempt := 0; attempt < cfg.MaxRetries; attempt++ {
		// 检查是否已经超时
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		err := fn()
		if err == nil {
			return nil
		}

		// 某些错误不应该retry
		if !isRetryable(err) {
			return err
		}

		// 计算下次retry的等待时间
		if attempt < cfg.MaxRetries-1 {
			select {
			case <-time.After(backoff):
				backoff = time.Duration(float64(backoff) * 1.5)
				if backoff > cfg.MaxBackoff {
					backoff = cfg.MaxBackoff
				}
			case <-ctx.Done():
				return ctx.Err()
			}
		}
	}

	return fmt.Errorf("max retries exceeded")
}

// Idempotency key去重
func (r *ToolRegistry) ExecuteIdempotent(ctx context.Context, idempotencyKey string, name string, input json.RawMessage) (interface{}, error) {
	// 检查是否已经执行过
	if result, exists := r.cache.Get(idempotencyKey); exists {
		return result, nil
	}

	result, err := r.Execute(ctx, name, input)
	if err == nil {
		r.cache.Set(idempotencyKey, result)
	}
	return result, err
}

验证清单:

  • Exponential backoff实现正确
  • Context timeout会中断retry
  • Idempotency key可以防止重复执行
  • 写操作都使用idempotency key

关键问题:

  1. 为什么retry要检查 ctx.Done()
  2. Backoff因子为什么是1.5而不是2.0?
  3. 某些错误(如权限错误)为什么不应该retry?

📌 Day 7: 第1周Mock和整理

你将学到:

  • 如何写项目文档
  • 如何做系统设计题
  • 如何评估自己的理解

交付物:

  • README更新
  • 3分钟项目介绍视频脚本
  • 10个测试prompt的结果
  • docs/agent-runtime.md
  • 系统设计题:设计一个企业客服agent

系统设计题框架:

1. 需求澄清 - 用户数量?日均请求量? - 关键指标(延迟、成功率等)? - SLA要求? 2. API设计 - POST /chat 接收消息 - 返回structured response - 支持流式响应吗? 3. Agent Runtime - LLM客户端(超时、retry、fallback) - Tool执行(并发控制、timeout) - 状态管理 4. 数据存储 - 对话历史? - 工具执行结果缓存? - Audit log? 5. 可观测性 - Tracing(request_id传播) - Metrics(延迟、工具调用次数) - 日志(结构化JSON) 6. 扩展性 - 支持多个LLM吗? - 支持自定义工具吗? - 支持权限控制吗?

验证清单:

  • 可以讲清Agent Loop的每一步
  • 可以解释为什么这样设计
  • 可以应对"如果XXX怎么办"的问题
  • 代码质量好到可以show off

📊 第1周学习成果检验

完成Day 1-7后,你应该能:

代码能力

  • 用Go写HTTP服务
  • 理解并使用context.Context
  • 设计interface解耦
  • 实现retry和idempotency

系统设计能力

  • 理解Agent Loop的完整流程
  • 能识别故障点(工具超时、LLM错误等)
  • 能做基本的容错设计

Go特有知识

  • context的几种创建方式
  • goroutine和channel的安全使用
  • Interface设计的价值
  • 错误处理的Go风格

问题解决

  • 没有工具时agent应该怎么办?
  • 工具返回错误时怎么恢复?
  • 如何防止tool call的重复执行?

🔗 Day 1-7代码关联

cmd/api/main.go └─ internal/server/server.go (Day 1 Router) ├─ internal/server/handler.go │ └─ internal/llm/openai_client.go (Day 2) │ └─ (Day 4 Agent Loop调用) │ ├─ internal/tools/registry.go (Day 3) │ └─ (Day 4 Agent Loop调用) │ └─ internal/agent/runtime.go (Day 4) ├─ (Day 5 Output validation) └─ internal/retry/retry.go (Day 6) internal/tools/ ├── search_knowledge_base.go (Day 3 Mock) ├── get_ticket_status.go (Day 3 Mock) └── create_ticket_draft.go (Day 3 Mock)

💪 下周预告

Week 2进入RAG领域:

  • 文档解析和chunking
  • Embedding + Vector search
  • Hybrid retrieval(结合关键词搜索)
  • Rerank和citation

v0.2会是一个完整的RAG系统,远超一般chatbot的复杂度。


⏱ 推荐时间分配

日期 Task 时间
Day 1上 看教材 + 建项目 1h
Day 1下 完成代码 + 调试 2h
Day 2上 看教材 + OpenAI SDK 1.5h
Day 2下 完成LLMClient + 测试 1.5h
Day 3 Tool Registry实现 3h
Day 4 Agent Loop(关键日) 4h
Day 5 Structured output 2h
Day 6 Retry + Idempotency 3h
Day 7 整理 + 系统设计题 3h

周总计: 20.5小时

最强的两天是Day 4(Agent Loop)和Day 6(Retry)。这两天要特别专注。


🎁 Day 7之后你有什么

  • 一个可以工作的v0.1 agent服务
  • 理解了Agent Loop的设计
  • 准备好进入RAG领域
  • 第一份可以show的代码

这时候可以往resume里加:

Designed and implemented a production-grade Go agent runtime with LLM integration, tool execution, and error recovery mechanisms.

实话讲,Week 1完成后,就是一个有模有样的项目了。

开始Day 1吧!💪