💡 一句话核心:不要直接把OpenAI SDK散落在handler里——用interface把"LLM调用"这件事抽象成你自己的领域概念,让生产代码既可替换、可测试,又能做好错误分类。
LLMClient interfaceOpenAIClient / MockClient / CachingClient 三种形态引导问题:
/chat 写单元测试——真的每次都发请求到OpenAI吗?要花多少钱?网络不通怎么办?答案揭示:
你应该理解:
引导问题:
Chat(messages []Message) string,后面想加tool calling怎么办?答案揭示:
引导问题:
答案揭示:
| 错误类型 | 典型原因 | 可重试? | 策略 |
|---|---|---|---|
| RateLimit | 429 | ✅ | 指数退避 |
| Timeout | ctx cancel / 网络 | ✅ | 限次重试 |
| InvalidRequest | 400 / schema错 | ❌ | 立即失败 + 报警 |
| ServerError | 5xx | ✅ | 少量重试 |
反思题:
Generate 的第一个参数一定要是 context.Context?(超时、取消、trace_id)*GenerateRequest 而不是值?(避免大结构体拷贝 + 允许未来扩展中间件修改)关键点:
Retryable 字段让上层无脑判断要不要重试Unwrap(),可以用标准库的 errors.Is/As问题: 为什么要把 classifyOpenAIError 独立成一个函数?
(答:纯函数易测;未来可以对它做单元测试,覆盖所有错误分支)
使用示例(测试代码里):
反思:
测试时传MockClient,生产传OpenAIClient,换厂商时传ClaudeClient。
每层职责单一,可独立测试、独立替换。
context.Context 是Go里传递超时/取消/trace的唯一正道ctx 到struct里(反模式)temperature=0 + 固定seed + 无tool → 幂等,可缓存GenerateRequest 和 GenerateResponse 的所有字段ErrorType,哪个 Retryable=trueMockClient 和 CachingClient 分别解决什么问题internal/llm/types.go:interface + 所有数据结构internal/llm/errors.go:LLMError + IsRetryableinternal/llm/openai.go:真实OpenAI调用 + 错误分类internal/llm/mock.go:MockClient + 便捷constructorinternal/llm/cache.go:CachingClient(内存版)至少覆盖:RateLimit / Timeout / InvalidRequest / ServerError / Unknown 五种分支。
/chatOPENAI_API_KEY 读取keycurl -X POST localhost:8080/chat -d '{"message":"你好"}' 能收到真实回复ClaudeClient)Q1:为什么不直接用 openai.Client 本身作为interface?
A:它的方法太多、参数耦合OpenAI特定概念(如 tool_choice: "auto")。自己定义interface让你有领域语言,而不是"OpenAI方言"。
Q2:错误分类除了重试,还有什么用? A:
Q3:CachingClient会不会有并发问题?
A:用 sync.RWMutex 保护map即可。但要注意 cache stampede——同一个key短时间内多个请求同时miss,都打到上游。解决方案:singleflight(golang.org/x/sync/singleflight)。
Q4:为什么 GenerateRequest 不直接包含 SystemPrompt 字段?
A:SystemPrompt本质是 Messages[0] with role=system。多加字段会让API语义重复。保持 Messages是唯一的真相来源。
Q5:生产上timeout应该多长? A:依赖调用场景:
超时必须从外部注入(通过 context.WithTimeout),不要写死在client里。
题目: 给一个字符串,返回最长无重复字符子串的长度。
思路: 滑动窗口 + hashmap 记录字符最近出现位置。
复杂度: O(n) 时间,O(min(n, Σ)) 空间。
Agent上下文联想: 滑动窗口在chunk去重、token窗口裁剪里很常见。
题目: 判断 ()[]{} 括号字符串是否合法。
思路: 栈。
复杂度: O(n) / O(n)。
Agent上下文联想: 解析LLM输出的JSON arguments时,配套理解token/bracket平衡能帮你做fail-fast检测。
题目: 有序数组中查找target,返回下标。
思路: 标准二分,注意边界。
复杂度: O(log n) / O(1)。
Agent上下文联想: Embedding检索后的top-k排序、时间戳日志定位、cost预算二分逼近都会用到。
明天我们会:
ToolDefinition 完整结构(Name/Description/InputSchema/RiskLevel/Timeout/Execute)search_kb / create_ticket / get_user_info准备问题: