Week 1 Day 7:第1周Mock - 苏格拉底教学

💡 一句话核心:代码能跑不等于代码好用,能讲清楚才算真正掌握。今天是整理、验收、和模拟面试的一天——把6天的积累转化成面试竞争力。

今日目标

  • 验收 v0.1 完整交付物
  • 通过代码自查清单发现潜在bug
  • 完整回答一道系统设计题:"设计一个客服Agent系统"
  • 练习 30秒 和 2分钟 的项目pitch
  • 完成 Week 1 知识点自测(12道题)

第一部分:v0.1 交付物清单

代码文件清单

运行以下命令,确认每个文件都存在:

# 项目根目录
ls agent-runtime/

# 预期结构
agent-runtime/
├── cmd/
│   └── api/
│       └── main.go             # HTTP服务入口,graceful shutdown
├── internal/
│   ├── server/
│   │   ├── server.go           # chi router + middleware组装
│   │   └── handler.go          # ChatHandler(调用Agent Runtime)
│   ├── llm/
│   │   ├── openai_client.go    # OpenAI SDK封装,实现LLMClient interface
│   │   └── types.go            # Message, GenerateRequest, GenerateResponse
│   ├── tools/
│   │   ├── types.go            # ToolDefinition, ToolResult, RiskLevel
│   │   ├── schema.go           # JSON Schema辅助函数
│   │   ├── registry.go         # Registry(Register/Get/Execute/List)
│   │   └── builtin/
│   │       ├── search_kb.go    # search_kb tool(RiskLow)
│   │       ├── create_ticket.go # create_ticket tool(RiskMedium)
│   │       └── get_user_info.go # get_user_info tool(RiskLow)
│   ├── agent/
│   │   ├── runtime.go          # Runtime, LoopState, Run(), processToolCalls()
│   │   ├── answer.go           # AgentAnswer struct(confidence, sources等)
│   │   └── validate.go         # 字段间约束校验
│   ├── retry/
│   │   ├── errors.go           # IsRetryable分类
│   │   └── backoff.go          # Exponential backoff + jitter
│   ├── idempotency/
│   │   ├── store.go            # MemStore或RedisStore
│   │   └── key.go              # KeyFor函数
│   └── config/
│       └── config.go           # 环境变量读取
├── go.mod
├── go.sum
└── Dockerfile

功能验收

# 1. 所有测试通过
go test ./... -race

# 2. 编译无报错
go build ./cmd/api

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

# 4. 在另一个终端测试endpoint
curl -s http://localhost:8080/health | jq .
# 期望:{"status":"ok"}

curl -s -X POST http://localhost:8080/chat \
  -H "Content-Type: application/json" \
  -d '{"message":"如何重置SSO密码?"}' | jq .
# 期望:有answer字段,total_iterations >= 1

# 5. 优雅关闭测试
# Ctrl+C,观察日志是否有 "shutdown signal received"

第二部分:代码自查 - 生产级Bug清单

🔍 自查1:Race Condition

Race Condition = 两个goroutine同时读写同一数据,没有锁保护。

高危代码模式:

// ❌ 危险:多个HTTP请求同时写 counter,没有锁
var ticketCounter int64

func createTicket() string {
    ticketCounter++  // 非原子操作!可能漏计
    return fmt.Sprintf("TICK-%d", ticketCounter)
}

// ✅ 正确:用 sync/atomic
var ticketCounter int64

func createTicket() string {
    id := atomic.AddInt64(&ticketCounter, 1)
    return fmt.Sprintf("TICK-%05d", id)
}

自查命令:

go test ./... -race
# 如果有race condition,运行时会报 "DATA RACE"

检查Registry:

// 确认 registry.go 里的每个公开方法都有锁保护
// Register → mu.Lock()
// Get → mu.RLock()(读锁,允许并发读)
// List → mu.RLock()
// Execute → 调用Get(有锁),Execute本身不需要额外锁

检查问题: 你的 idempotency.MemStore 里,AcquireOrGet 是原子的吗?CompleteAcquireOrGet 之间有TOCTOU(time-of-check-time-of-use)窗口吗?


🔍 自查2:Nil Panic

nil panic = 对nil指针解引用,是Go最常见的运行时崩溃。

高危场景:

// ❌ 危险:没检查err就直接用resp
resp, err := llmClient.Generate(ctx, req)
answer := resp.Content  // 如果err != nil,resp可能是nil!

// ✅ 正确
resp, err := llmClient.Generate(ctx, req)
if err != nil {
    return nil, err
}
answer := resp.Content  // 现在安全

检查每个工具的Execute函数:

// ❌ 危险:args可能是nil
func execute(ctx context.Context, raw json.RawMessage) (*ToolResult, error) {
    var args MyArgs
    json.Unmarshal(raw, &args)  // 忽略了error!
    result := args.Field + "suffix"  // 如果Unmarshal失败,Field是零值,可能ok
    // 但如果args是指针类型,就会nil panic
}

// ✅ 正确
func execute(ctx context.Context, raw json.RawMessage) (*ToolResult, error) {
    if raw == nil {
        return &ToolResult{Error: "arguments required"}, nil
    }
    var args MyArgs
    if err := json.Unmarshal(raw, &args); err != nil {
        return &ToolResult{Error: "invalid arguments: " + err.Error()}, nil
    }
    // ...
}

检查LLM响应解析:

// ❌ 危险:没检查Choices是否为空
content := resp.Choices[0].Message.Content  // 如果Choices为空,panic!

// ✅ 正确
if len(resp.Choices) == 0 {
    return nil, fmt.Errorf("no choices in response")
}
content := resp.Choices[0].Message.Content

🔍 自查3:Goroutine泄漏

Goroutine泄漏 = goroutine启动了但永远不会退出,内存持续增长。

高危模式:

// ❌ 危险:goroutine永远不退出
go func() {
    for {
        // 做一些事,但没有退出条件
        time.Sleep(1 * time.Second)
    }
}()

// ✅ 正确:通过 ctx 控制生命周期
go func() {
    for {
        select {
        case <-ctx.Done():
            return  // 上层取消时退出
        case <-time.After(1 * time.Second):
            // 做一些事
        }
    }
}()

检查 gc goroutine(idempotency.MemStore里):

// ❌ 危险:gc goroutine会随着MemStore一起被GC吗?不会!
func NewMemStore(ttl time.Duration) *MemStore {
    s := &MemStore{...}
    go s.gc()  // 这个goroutine会在MemStore被GC后继续运行!
    return s
}

// ✅ 正确:提供Stop方法,或者用context控制
func NewMemStore(ctx context.Context, ttl time.Duration) *MemStore {
    s := &MemStore{...}
    go func() {
        ticker := time.NewTicker(time.Minute)
        defer ticker.Stop()
        for {
            select {
            case <-ctx.Done():
                return  // 上层shutdown时退出
            case <-ticker.C:
                s.cleanup()
            }
        }
    }()
    return s
}

检查工具,里的goroutine:

  • 搜索代码里所有 go func()go 开头的行
  • 每一个都要问:它什么时候退出?退出条件是什么?

🔍 自查4:Deadlock

Deadlock = 两个操作互相等对方,永远卡住。

高危模式1:对同一个mutex加两次锁

// ❌ 危险:Register调用了Get,而Get也加锁
func (r *Registry) Register(t *ToolDefinition) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    if _, exists := r.Get(t.Name); exists {  // Get内部也会加RLock!死锁!
        return fmt.Errorf("already registered")
    }
    // ...
}

// ✅ 正确:在锁内直接操作map,不调用Get
func (r *Registry) Register(t *ToolDefinition) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    if _, exists := r.tools[t.Name]; exists {  // 直接访问map,不加锁
        return fmt.Errorf("already registered")
    }
    // ...
}

高危模式2:channel满了没有消费者

// ❌ 危险:如果没有人接收sigChan,发送方会阻塞
sigChan := make(chan os.Signal)  // 无缓冲!
signal.Notify(sigChan, syscall.SIGINT)

// ✅ 正确:设置缓冲=1,OS信号不会阻塞
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

运行时检测:

# 如果程序卡住了,可以发送SIGQUIT查看goroutine堆栈
kill -SIGQUIT <pid>
# 或者在代码里import _ "net/http/pprof" 然后访问 /debug/pprof/goroutine

第三部分:系统设计题 - "设计一个客服Agent系统"

这是面试中最常见的系统设计题形式。完整回答需要8-12分钟。先读完,再合上文件练习自己讲。

第1步:需求澄清(2分钟)

你应该主动问:

  • 用户规模?日均多少对话?(假设:1万用户,10万对话/天)
  • 每次对话平均几条消息?(假设:5条)
  • 需要支持哪些功能?(查FAQ、创建工单、查订单状态)
  • 是否需要多轮对话记忆?(暂时不需要,每次对话独立)
  • SLA要求?(P99 < 5秒,可用率 > 99.9%)
  • 需要人工接管吗?(需要:confidence=low的场景)

第2步:功能需求(明确边界)

核心功能(必须做):

  1. 用户发送消息 → Agent自动回答
  2. Agent可以调用工具(查FAQ、创建工单)
  3. 低置信度回答自动转人工
  4. 所有对话和工具调用有审计日志

非功能需求:

  1. P99延迟 < 5秒(含LLM调用)
  2. 系统崩溃后可恢复,不丢失进行中的工单
  3. 支持10倍流量增长(水平扩展)

第3步:整体架构

用户 │ │ HTTPS ▼ ┌──────────────────┐ │ API Gateway │ <- 认证、限流、路由 │ (nginx/cloudflare)│ └────────┬─────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ Agent Service (Go) │ │ │ │ HTTP Handler │ │ │ │ │ ▼ │ │ Agent Runtime ──► LLM Client ──► OpenAI API │ │ │ │ │ ▼ │ │ Tool Registry │ │ ├── search_kb ──────────────► Vector DB (Qdrant) │ │ ├── create_ticket ──────────► Ticket DB (Postgres) │ │ └── get_user_info ──────────► User DB (Postgres) │ │ │ │ Structured Output + Validation │ │ │ │ │ ▼ │ │ Audit Logger ──────────────────► Audit DB (Postgres) │ └──────────────────────────────────────────────────────┘ │ │ confidence=low ▼ ┌──────────────────┐ │ Human Queue │ <- 人工客服看板 │ (Postgres/Redis) │ └──────────────────┘

第4步:关键权衡(面试官最关心这里)

权衡1:Stateless vs Stateful Agent

问题: messages历史存在哪里?

方案 优点 缺点
内存(当前v0.1) 最快,无额外存储 服务重启消失;不能水平扩展
Redis(Session存储) 快,跨实例共享 成本高;需处理失效
Postgres(DB) 持久化,审计 延迟较高;需要连接池

推荐: v0.1用内存,v0.2迁移到Redis,生产用Postgres审计+Redis缓存。


权衡2:同步 vs 异步

问题: 如果LLM调用+工具执行需要8秒,用户能接受吗?

方案 用户体验 复杂度
同步(当前) 等待,可能超时 简单
Streaming(SSE) 实时看到token输出 中等
异步(队列) 立即返回job_id,轮询结果 复杂

推荐: 先实现Streaming(OpenAI支持stream=true),让用户看到实时输出,感知延迟大幅降低。


权衡3:工具调用的并发

问题: 如果一次LLM响应要求调用3个工具,应该串行还是并行?

// 当前:串行(简单,易调试)
for _, call := range calls {
    result := registry.Execute(ctx, call.Name, call.Arguments)
    // ...
}

// 优化:并行(更快,但结果汇聚复杂)
var wg sync.WaitGroup
results := make([]ToolResult, len(calls))
for i, call := range calls {
    wg.Add(1)
    go func(i int, call ToolCall) {
        defer wg.Done()
        results[i] = registry.Execute(ctx, call.Name, call.Arguments)
    }(i, call)
}
wg.Wait()

推荐: 工具间无依赖时并行,有依赖时串行。v0.1先串行,v0.3加并行优化。


权衡4:LLM Provider失败

问题: OpenAI宕机了,系统怎么办?

// 两层策略:
// 1. Retry with backoff(Day 6实现)
// 2. Fallback to另一个Provider

type FallbackLLMClient struct {
    primary  LLMClient  // OpenAI
    fallback LLMClient  // Anthropic Claude / Azure OpenAI
}

func (f *FallbackLLMClient) Generate(ctx context.Context, req GenerateRequest) (*GenerateResponse, error) {
    resp, err := f.primary.Generate(ctx, req)
    if err != nil && isProviderError(err) {
        slog.Warn("primary llm failed, using fallback", "error", err)
        return f.fallback.Generate(ctx, req)
    }
    return resp, err
}

第5步:可观测性

面试中必须主动提到,不要等面试官问:

// 三大支柱:Logging + Metrics + Tracing

// 1. Structured Logging(已实现)
slog.Info("agent.loop.iteration",
    "request_id", requestID,
    "iteration", state.Iteration,
    "tool_calls_this_round", len(calls),
)

// 2. Metrics(Prometheus格式)
// - agent_loop_iterations_total(histogram)
// - tool_execution_duration_seconds(histogram,按tool_name分组)
// - llm_call_duration_seconds(histogram)
// - agent_errors_total(counter,按error_type分组)

// 3. Distributed Tracing(OpenTelemetry)
// - 每个HTTP请求一个trace_id
// - 每次LLM调用一个span
// - 每次工具调用一个子span
// 最终在Jaeger/Grafana Tempo里看到完整调用链

完整回答模板(8分钟版本)

第1分钟:澄清需求 "在我开始设计之前,我想先确认几个需求: 日均对话量大概多少?是否需要多轮记忆?SLA要求如何? ......(根据你的回答)好,我的设计目标是:支持10万对话/天, 每次对话独立,P99 < 5秒。" 第2分钟:整体架构图 "我会分成这几个核心模块:API层、Agent Runtime、工具系统、存储层。 主要数据流是:用户请求进来→Agent Loop循环→调用工具→返回结构化答案→ 低置信度走人工队列。" (在白板上画出上面的架构图) 第3-5分钟:每个组件详解 "Agent Runtime是核心,它维护一个消息历史, 每次调用LLM判断是否需要工具,工具结果喂回LLM, 直到得到最终答案或超过max_iterations限制。" 第6-7分钟:关键权衡 "这里有几个重要的设计权衡: 第一,消息历史的存储,我选择Redis+Postgres…… 第二,工具调用的并发,暂时串行,后续优化…… 第三,LLM Provider的failover……" 第8分钟:可观测性和扩展性 "生产环境里,我会加三大支柱: structured logging记录每次循环的状态; Prometheus metrics监控延迟和错误率; OpenTelemetry tracing追踪完整请求链路。 扩展性方面,Agent Service是无状态的,可以水平扩展, 唯一的状态在Redis和DB里。"

第四部分:Pitch 模板

30秒版本(电梯间遇到面试官)

"我用Go实现了一个生产级的Agent Runtime, 核心是一个完整的Agent Loop:用户输入进来, LLM判断是否需要调用工具,工具执行结果喂回LLM, 循环直到得到最终答案。 整个系统包括Tool Registry(带风险等级和超时控制)、 Structured Output(带字段约束验证)、 以及Retry/Idempotency机制保证工具调用的可靠性。 我用这个系统实现了一个客服Agent场景作为验证。"

2分钟版本(面试自我介绍)

"过去一周,我实现了一个完整的Go Agent系统,目标是支持企业客服场景。 架构方面,核心是一个Agent Loop: 用户输入进来,LLM判断是否需要调用工具, 工具执行结果作为下一轮LLM的输入,循环最多10轮, 直到得到最终答案或超过限制。 技术选型方面,我用了几个关键设计: 第一,Tool Registry抽象了工具系统,每个工具带风险等级和超时控制, 执行层有panic recovery保证不崩溃; 第二,Structured Output让LLM的回答有明确的字段契约, 包括confidence、sources等,带validation确保业务约束; 第三,Retry加Idempotency保证即使LLM或工具临时失败, 系统能可靠恢复,且写操作不会重复执行。 我在实现过程中踩过几个坑: 消息历史的顺序必须严格遵循OpenAI规范,否则API会报错; 工具调用的错误处理必须区分'系统错误'和'业务错误'; race condition在并发测试前根本看不出来。 这是我作为Infrastructure Engineer的第一个实战项目, 周末准备把它部署到Docker并加上基本的metrics监控。"

第五部分:Week 1 知识点自测(12道题)

规则: 合上文档,在草稿纸上回答。每道题给自己3分钟。答不上来的标记"需复习"。


基础Go(Day 1-2)

Q1. context.WithTimeoutcontext.WithDeadline 的区别是什么?什么时候用哪个?

参考答案: WithTimeout接受一个相对时长(如5秒),WithDeadline接受一个绝对时间点。大多数情况用WithTimeout更方便;如果要对齐到某个绝对截止时间(如凌晨2点),用WithDeadline。内部实现上WithTimeout就是用Now()+duration调用WithDeadline。


Q2. HTTP Server 的 ServeHTTP 方法签名是什么?Middleware 是如何利用它实现链式调用的?

参考答案: ServeHTTP(w http.ResponseWriter, r *http.Request)。Middleware接受一个http.Handler,返回一个新的http.Handler。在新Handler内部,先做前置处理,再调用next.ServeHTTP(w, r),再做后置处理。这样可以链式组合多个Middleware。


Q3. 为什么 Graceful Shutdown 需要给30秒?如果直接 os.Exit(0) 会有什么问题?

参考答案: 正在处理的请求需要时间完成(尤其是LLM调用可能需要几秒)。直接Exit会强制中断所有连接,导致:用户收到连接重置错误;工具调用可能已执行但没有返回结果(创建了工单但用户不知道);数据库事务可能未提交。30秒是一个经验值,足够大多数请求完成。


LLM Client(Day 2)

Q4. 为什么 LLMClient 要定义成 interface 而不是直接用 *openai.Client?举出两个具体好处。

参考答案:

  1. 可替换性:后续可以无缝切换到Anthropic、Azure OpenAI或本地模型,只需实现同一interface;
  2. 测试友好:在单元测试里mock这个interface,不需要真实的API Key和网络调用,测试快且稳定;
  3. (加分)统一错误处理:wrapper层可以把各家SDK的错误统一转换成内部错误类型。

Tool System(Day 3)

Q5. ToolResult 有两个错误表达:返回值里的 errorToolResult.Error 字段。它们分别用于什么情况?

参考答案: 返回值里的error代表系统级错误(网络断了、数据库崩了),调用方可能需要重试或报警,不应该直接喂给LLM;ToolResult.Error代表业务级错误(用户不存在、参数格式错误),应该把这个信息喂给LLM,让LLM根据错误信息做出适当回应(道歉/重试/转人工)。


Q6. Registry.Execute 里用了 defer func() { if r := recover() } 来捕获panic。为什么工具里会发生panic?举一个具体例子。

参考答案: 工具接受LLM的任意输入,LLM可能产生幻觉传入奇怪的参数。比如:工具期望user_id是"u_001"格式,LLM传了nil或者空字符串,工具内部代码做userMap[args.UserID]没有nil检查,或者数组越界,就会panic。用recover兜底确保一个工具的panic不会崩掉整个服务。


Agent Loop(Day 4)

Q7. 在 Agent Loop 里,为什么要先把 assistant 消息(带tool_calls)加入 messages,再执行工具,再把工具结果加入 messages?如果顺序错了会怎样?

参考答案: OpenAI API要求消息顺序严格遵循"assistant消息 → 对应的tool消息"格式。如果先加tool消息再加assistant消息,或者没有assistant消息就直接加tool消息,API会返回400错误。顺序是:[user] → [assistant with tool_calls] → [tool] → [assistant] → ...


Q8. 如果 Agent Loop 里 LLM 每次都调用同一个工具,max_iterations 会生效。但有没有更好的方式提前发现这个问题?

参考答案:processToolCalls里维护一个toolCallCount map[string]int,记录每个工具被调用了多少次。如果某个工具调用次数超过阈值(如3次),向messages里注入一条警告消息:"You've called 'X' 3 times. Please provide a final answer with current information."这样LLM可能会意识到自己在循环并给出最终答案,而不是等到第10次才终止。


Structured Output(Day 5)

Q9. AgentAnswer 的 confidence 字段为什么用 "low"|"medium"|"high" 枚举而不是 float64(0-1)?

参考答案:

  1. LLM输出0.85和0.87之间没有实际意义上的区别,浮点数给出了虚假的精确感;
  2. 业务规则用枚举更清晰:if confidence == "low" { route_to_human() }if confidence < 0.6 { route_to_human() } 更可读、更稳定(阈值0.6是主观的);
  3. Eval时更容易标注和对比:人工标注"high"还是"low"比标注0.85更快,模型训练时标签也更clean。

Retry / Idempotency(Day 6)

Q10. Exponential Backoff 加了 Jitter 之后能解决什么问题?不加 Jitter 会怎样?

参考答案: 不加Jitter时,如果1000个客户端同时失败,它们会在相同的延迟后同时重试——这就是Thundering Herd(惊群)问题,下游会再次被瞬间1000个请求打爆。加Jitter(随机化等待时间)后,1000个客户端的重试时间被分散到一个时间窗口内,下游压力平滑化。


Q11. Idempotency Key 应该由客户端生成还是服务端生成?为什么不能用 uuid.New() 在每次重试时生成新的key?

参考答案: 由客户端生成。服务端没有办法知道"这次请求和上次请求是同一个操作"。用uuid.New()每次重试生成不同的key,服务端会认为是全新操作,从而创建重复工单——这完全违背了幂等的目的。正确做法是基于业务语义生成key:sha256(user_id + request_id + operation),同一用户同一请求的重试会生成相同的key。


综合

Q12. 你的 v0.1 系统如果同时收到100个并发请求,会有什么问题?说出至少3个潜在瓶颈。

参考答案:

  1. LLM并发限制:OpenAI API有rate limit(tokens/min, requests/min),100个并发可能触发429,需要限流或队列;
  2. 内存消耗:每个请求的messages历史存在内存里,100个请求同时维护10轮对话的历史,内存用量较大;
  3. 工具执行超时:100个并发,有些工具调用可能排队(比如数据库连接池耗尽),导致工具超时率上升;
  4. goroutine数量:Go的goroutine轻量但不是免费的,100并发×每次循环的goroutine开销需要评估;
  5. 没有限流保护:没有实现per-user rate limiting,一个恶意用户可以打爆系统。

第六部分:作业

任务1:完整运行测试

  • go test ./... -race 全绿,无race condition报告
  • go build ./cmd/api 编译成功
  • 三个curl场景(无工具/单工具/多步骤)都有正确响应

任务2:找到并修复1个真实bug

仔细看自己的代码,找到一个上面代码自查清单里描述的问题,修复它,写一条git commit记录你修复了什么。

任务3:练习Pitch

  • 对着镜子讲30秒版本,不超过35秒,计时
  • 对着镜子讲2分钟版本,不超过2分30秒,计时
  • 找一个人(朋友/室友/录视频)讲系统设计题,用计时器控制在10分钟内

任务4:自测结果记录

# 创建自测记录文件
cat > weekplan/week1_selftest.md << 'EOF'
## Week 1 自测结果

| Q | 知识点 | 能独立回答? | 需要复习 |
|---|-------|------------|---------|
| 1 | context超时 | ✅/❌ | |
| 2 | Middleware链 | ✅/❌ | |
| ... | ... | ... | |

## 需要重点复习的点

## Week 2 预习问题
EOF

第七部分:常见面试陷阱

陷阱1:被追问细节时,说"这是框架帮我做的"

错误回答:"Middleware是chi框架实现的,我就直接用了。"

正确回答:"Middleware本质上是一个函数,接受http.Handler返回新的http.Handler。chi的Middleware和我自己写的func Logging(next http.Handler) http.Handler是同一个接口。原理是:每次请求进来,先执行wrapper里的前置代码,调用next.ServeHTTP,再执行后置代码。"


陷阱2:系统设计题跳过需求澄清直接画架构

面试官问你这个问题不是要看你背了哪个架构,而是要看你能不能识别关键约束。日均10万请求和日均100万请求的架构完全不同,先问清楚。


陷阱3:说"我没有遇到并发问题"

错误回答:"我没有测试过并发,但代码看起来应该没问题。"

正确回答:"我用go test -race检测了race condition。Registry里的map用了RWMutex。MemStore的AcquireOrGet用了Mutex保证原子性。ticketCounter用了atomic.AddInt64。这些是我主动想到的;实际上我也想到了gc goroutine的生命周期问题,用context传入控制它的退出。"


陷阱4:说不清楚Agent Loop的终止条件

面试官:"如果Agent一直在调工具,不给最终答案,怎么办?"

错误回答:"有max_iterations限制。"(太简单)

正确回答:"有三层保护:第一,max_iterations(默认10次),超过立即终止并返回错误给用户;第二,context deadline,如果HTTP请求有超时(比如30秒),ctx会被cancel,LLM调用和工具调用都会中断;第三,对于循环检测,我会记录每个工具的调用次数,超过3次注入提示消息让LLM给出最终答案。这三层配合,基本能覆盖所有异常情况。"


第八部分:Week 2 RAG 预告

恭喜完成Week 1!你现在有了一个真实可运行的Agent系统。Week 2我们进入RAG(Retrieval-Augmented Generation)领域——让Agent不再依赖LLM的"记忆",而是能从外部知识库检索真实信息来回答。

Week 2 核心问题(思考,下周揭晓):

  1. 现在你的search_kb是一个假的字典查找,真实的知识库有成千上万条文档,怎么快速找到最相关的?
  2. "语义相似"和"关键词匹配"有什么区别?一个FAQ: 如何重置密码,用户问我忘了登录凭证怎么办,关键词匹配得到吗?
  3. 你把一篇10000字的文档丢给LLM,和把最相关的3段(各300字)丢给LLM,哪个效果更好?为什么?
  4. Vector Database是什么?它和普通数据库有什么本质区别?

Week 2 交付物目标:

  • v0.2:接入Qdrant Vector DB,实现真实的语义搜索
  • 文档分割(Chunking)、向量化(Embedding)、检索(Search)完整管线
  • Eval数据集:10条query,评估答案质量

开始Day 8前,先把Day 1-6的代码推到Git(最好是private repo),让它成为你第一份可以展示的Go项目。