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 是原子的吗?Complete 和 AcquireOrGet 之间有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步:功能需求(明确边界)
核心功能(必须做):
- 用户发送消息 → Agent自动回答
- Agent可以调用工具(查FAQ、创建工单)
- 低置信度回答自动转人工
- 所有对话和工具调用有审计日志
非功能需求:
- P99延迟 < 5秒(含LLM调用)
- 系统崩溃后可恢复,不丢失进行中的工单
- 支持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.WithTimeout 和 context.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?举出两个具体好处。
参考答案:
- 可替换性:后续可以无缝切换到Anthropic、Azure OpenAI或本地模型,只需实现同一interface;
- 测试友好:在单元测试里mock这个interface,不需要真实的API Key和网络调用,测试快且稳定;
- (加分)统一错误处理:wrapper层可以把各家SDK的错误统一转换成内部错误类型。
Tool System(Day 3)
Q5. ToolResult 有两个错误表达:返回值里的 error 和 ToolResult.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)?
参考答案:
- LLM输出0.85和0.87之间没有实际意义上的区别,浮点数给出了虚假的精确感;
- 业务规则用枚举更清晰:
if confidence == "low" { route_to_human() } 比 if confidence < 0.6 { route_to_human() } 更可读、更稳定(阈值0.6是主观的);
- 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个潜在瓶颈。
参考答案:
- LLM并发限制:OpenAI API有rate limit(tokens/min, requests/min),100个并发可能触发429,需要限流或队列;
- 内存消耗:每个请求的messages历史存在内存里,100个请求同时维护10轮对话的历史,内存用量较大;
- 工具执行超时:100个并发,有些工具调用可能排队(比如数据库连接池耗尽),导致工具超时率上升;
- goroutine数量:Go的goroutine轻量但不是免费的,100并发×每次循环的goroutine开销需要评估;
- 没有限流保护:没有实现per-user rate limiting,一个恶意用户可以打爆系统。
第六部分:作业
任务1:完整运行测试
任务2:找到并修复1个真实bug
仔细看自己的代码,找到一个上面代码自查清单里描述的问题,修复它,写一条git commit记录你修复了什么。
任务3:练习Pitch
任务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 核心问题(思考,下周揭晓):
- 现在你的
search_kb是一个假的字典查找,真实的知识库有成千上万条文档,怎么快速找到最相关的?
- "语义相似"和"关键词匹配"有什么区别?一个
FAQ: 如何重置密码,用户问我忘了登录凭证怎么办,关键词匹配得到吗?
- 你把一篇10000字的文档丢给LLM,和把最相关的3段(各300字)丢给LLM,哪个效果更好?为什么?
- Vector Database是什么?它和普通数据库有什么本质区别?
Week 2 交付物目标:
v0.2:接入Qdrant Vector DB,实现真实的语义搜索
- 文档分割(Chunking)、向量化(Embedding)、检索(Search)完整管线
- Eval数据集:10条query,评估答案质量
开始Day 8前,先把Day 1-6的代码推到Git(最好是private repo),让它成为你第一份可以展示的Go项目。