从零开始手动实现 AI Agent(四)
🎯 从"能用"到"好用"——引入 LangChain 生态,提升检索质量; RAG 技术升级,更高效,更准确的检索效率
目录
AI Agent 进阶:框架重构与 RAG 优化
🎯 从"能用"到"好用"——引入 LangChain 生态,提升检索质量
前情回顾:上篇文章的不足
在上一篇文章《RAG 检索增强生成》中,我们成功实现了:
- ✅ 向量数据库存储和检索
- ✅ Markdown 文档智能切分
- ✅ RAG 集成到 Agent 对话流程
但是,这个实现存在几个问题:
问题 1:代码耦合严重
所有逻辑都堆在一个文件里,LLM 调用、记忆管理、消息处理混在一起:
// 旧代码:一个庞大的 ChatAgent
type ChatAgent struct {
APIKey string
APIURL string
Model string
FullHistory []Message // 记忆管理
OptimizedMessages []Message // 记忆优化
HTTPClient *http.Client // HTTP 调用
MaxTokens int // Token 控制
// ... 更多字段
}
// 一个方法做太多事情
func (ca *ChatAgent) SendMessage(userMessage string) (string, error) {
// RAG 检索
// 消息构建
// Token 优化
// 摘要生成
// HTTP 请求
// 响应解析
// ... 300 行代码
}
这种设计难以维护、测试和扩展。
问题 2:缺乏标准化
我们自己实现了所有东西,但:
- 消息格式不统一
- 无法复用社区组件
- 每次换 LLM 都要改代码
问题 3:检索质量不够好
向量相似度检索存在局限:
- 语义相似 ≠ 问题相关
- Top-K 结果可能包含噪音
- 缺乏精排机制
本次更新:两大改进
本次更新解决了上述问题:
- 引入 LangChain 生态:使用
langchaingo框架重构 - 添加 Rerank 重排:提升检索准确性
什么是 LangChain?
LangChain 是构建 LLM 应用的标准框架,提供:
- 📦 统一抽象:LLM、Memory、Tools 的标准接口
- 🔗 链式组合:将多个组件串联成工作流
- 🧩 丰富生态:大量现成的组件和集成
LangChain 在各语言的实现
| 语言 | 项目 | Stars | 说明 |
|---|---|---|---|
| Python | langchain | 100k+ | 官方主实现 |
| JavaScript | langchainjs | 15k+ | 官方 JS 版 |
| Go | langchaingo | 5k+ | 社区实现,我们用这个 |
| Java | langchain4j | 6k+ | Java 社区实现 |
为什么选择 langchaingo?
// 使用 langchaingo 的好处
import "github.com/tmc/langchaingo/llms"
// 1. 统一的消息格式
message := llms.MessageContent{
Role: llms.ChatMessageTypeHuman,
Parts: []llms.ContentPart{
llms.TextPart("你好"),
},
}
// 2. 标准的 LLM 接口
type Model interface {
GenerateContent(ctx context.Context, messages []MessageContent, ...) (*ContentResponse, error)
Call(ctx context.Context, prompt string, ...) (string, error)
}
// 3. 开箱即用的组件
// - Memory(记忆管理)
// - Chains(链式调用)
// - Agents(代理)
// - Tools(工具)
重构后的架构
新的项目结构
agent/
├── agent.go # ChatAgent 核心(精简后)
├── llm.go # 🆕 LLM 提供者封装
├── llm_wrapper.go # 🆕 自定义 LLM 包装器
├── memory.go # 🆕 记忆管理器
├── cli.go # 命令行交互
└── types.go # 类型定义
架构图
┌─────────────────────────────────────────────────────────────┐
│ ChatAgent (精简后) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ LLMProvider │ │ MemoryManager │ │
│ │ LLM 提供者 │ │ 记忆管理器 │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ CustomLLMWrapper│ │ langchaingo │ │
│ │ 自定义 LLM 包装 │ │ MessageContent │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ RAG Retriever │ │
│ │ (向量检索 + Rerank 重排) │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
职责分离
| 组件 | 职责 | 代码行数 |
|---|---|---|
ChatAgent |
协调各组件,处理对话流程 | ~150 行 |
LLMProvider |
封装 LLM 调用 | ~40 行 |
CustomLLMWrapper |
处理 API 请求/响应 | ~160 行 |
MemoryManager |
管理对话历史和优化 | ~140 行 |
总代码量:从原来的 500+ 行单文件,拆分为 4 个模块,每个不超过 160 行。
核心组件详解
组件 1:LLMProvider(LLM 提供者)
// agent/llm.go
// LLMProvider 封装 langchaingo LLM 接口
type LLMProvider struct {
llm llms.Model
}
// NewLLMProvider 创建一个新的 LLM 提供者
func NewLLMProvider(apiKey, apiURL, model string) (*LLMProvider, error) {
// 使用自定义包装器,绕过 langchaingo 的角色检查
llm := NewCustomLLMWrapper(apiKey, apiURL, model)
return &LLMProvider{llm: llm}, nil
}
// Call 调用 LLM 生成响应
func (p *LLMProvider) Call(ctx context.Context, messages []llms.MessageContent) (string, error) {
response, err := p.llm.GenerateContent(ctx, messages)
if err != nil {
return "", fmt.Errorf("failed to generate content: %w", err)
}
if len(response.Choices) == 0 {
return "", fmt.Errorf("no choices in response")
}
return response.Choices[0].Content, nil
}
设计要点:
- 遵循 langchaingo 的
llms.Model接口 - 隐藏底层实现细节
- 统一的调用方式
组件 2:CustomLLMWrapper(自定义包装器)
为什么需要自定义包装器?
问题:langchaingo 对某些 API(如豆包)的角色名称检查过于严格
// langchaingo 期望的角色
llms.ChatMessageTypeHuman // "human"
llms.ChatMessageTypeAI // "ai"
llms.ChatMessageTypeSystem // "system"
// 但有些 API 期望
"user" // 而不是 "human"
"assistant" // 而不是 "ai"
解决方案:自定义包装器,在发送前转换角色名称
// agent/llm_wrapper.go
// CustomLLMWrapper 包装 langchaingo 的 LLM,绕过角色检查
type CustomLLMWrapper struct {
apiKey string
apiURL string
model string
client *http.Client
}
// GenerateContent 实现 llms.Model 接口
func (w *CustomLLMWrapper) GenerateContent(ctx context.Context, messages []llms.MessageContent, options ...llms.CallOption) (*llms.ContentResponse, error) {
// 将 langchaingo 的消息格式转换为 API 期望的格式
apiMessages := make([]map[string]string, 0, len(messages))
for _, msg := range messages {
// 提取文本内容
var content string
for _, part := range msg.Parts {
if textContent, ok := part.(llms.TextContent); ok {
content += textContent.Text
}
}
// 🔑 关键:角色名称转换
role := string(msg.Role)
switch role {
case "human":
role = "user" // human → user
case "ai":
role = "assistant" // ai → assistant
case "system":
role = "system" // 保持不变
}
apiMessages = append(apiMessages, map[string]string{
"role": role,
"content": content,
})
}
// 构建请求并发送...
// 返回 langchaingo 格式的响应
return &llms.ContentResponse{
Choices: []*llms.ContentChoice{
{Content: response.Choices[0].Message.Content},
},
}, nil
}
组件 3:MemoryManager(记忆管理器)
将记忆管理独立出来,职责单一:
// agent/memory.go
// MemoryManager 管理对话历史和记忆
type MemoryManager struct {
fullHistory []llms.MessageContent // 完整历史
optimizedMessages []llms.MessageContent // 优化后的消息
maxTokens int
keepRecentRounds int
summary string // 摘要
}
// AddMessage 添加消息到历史
func (m *MemoryManager) AddMessage(role string, content string) {
// 角色转换(兼容多种输入格式)
var chatRole llms.ChatMessageType
switch role {
case "system":
chatRole = llms.ChatMessageTypeSystem
case "user", "human":
chatRole = llms.ChatMessageTypeHuman
case "assistant", "ai":
chatRole = llms.ChatMessageTypeAI
default:
chatRole = llms.ChatMessageTypeHuman
}
msg := llms.MessageContent{
Role: chatRole,
Parts: []llms.ContentPart{
llms.TextPart(content),
},
}
m.fullHistory = append(m.fullHistory, msg)
}
// Optimize 优化消息列表,控制 token 数量
func (m *MemoryManager) Optimize(estimateTokens func(string) int) {
// ... 与之前的逻辑相同
// 但现在使用 langchaingo 的消息格式
}
组件 4:精简后的 ChatAgent
// agent/agent.go
// ChatAgent handles the chat functionality using langchaingo
type ChatAgent struct {
llmProvider *LLMProvider // LLM 调用
memoryManager *MemoryManager // 记忆管理
estimateTokens func(string) int // Token 估算
ragRetriever *rag.Retriever // RAG 检索
ragEnabled bool
}
// SendMessage 发送消息(精简后)
func (ca *ChatAgent) SendMessage(userMessage string) (string, error) {
ctx := context.Background()
// 1. RAG 检索
ragContext := ""
if ca.ragEnabled && ca.ragRetriever != nil {
context, _ := ca.ragRetriever.GetRelevantContext(userMessage, 5)
ragContext = context
}
// 2. 构建消息
finalMessage := userMessage
if ragContext != "" {
finalMessage = ragContext + "\n\nUser question: " + userMessage
}
ca.memoryManager.AddMessage("user", finalMessage)
// 3. 优化记忆
ca.memoryManager.Optimize(ca.estimateTokens)
// 4. 调用 LLM
response, err := ca.llmProvider.Call(ctx, ca.memoryManager.GetOptimizedMessages())
if err != nil {
return "", err
}
// 5. 保存回复
ca.memoryManager.AddMessage("assistant", response)
return response, nil
}
对比:重构前 vs 重构后
| 指标 | 重构前 | 重构后 |
|---|---|---|
| 单文件行数 | 500+ 行 | 最大 160 行 |
| 文件数量 | 1 个 | 4 个 |
| 职责 | 混合 | 单一 |
| 可测试性 | 差 | 好 |
| 可扩展性 | 差 | 好 |
Rerank:提升检索质量
什么是 Rerank?
向量检索的局限性:
查询: "如何解决登录失败问题"
向量检索结果(按相似度):
1. "登录失败的常见原因" 相似度: 0.89 ← 不是解决方案
2. "用户登录流程说明" 相似度: 0.85 ← 不相关
3. "登录失败解决方案汇总" 相似度: 0.82 ← 这才是用户想要的!
问题:语义相似 ≠ 问题最相关
解决方案:Rerank(重排)—— 用更精准的模型对初筛结果进行二次排序
向量检索(粗筛)──▶ Top 20 结果 ──▶ Rerank(精排)──▶ Top 5 结果
快速 召回多 准确
Rerank 的工作流程
┌─────────────────────────────────────────────────────────────┐
│ Rerank 工作流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户查询: "如何解决登录失败" │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ 1. 向量检索(粗筛) │ │
│ │ 返回 Top 20 相似文档 │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ 2. Rerank 模型(精排) │ │
│ │ 对每个文档计算相关性分数 │ │
│ │ Query + Doc → 0.0~1.0 │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ 3. 按新分数重新排序 │ │
│ │ 返回 Top 5 最相关文档 │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
实现:两种 Reranker
我们实现了两种 Reranker:
1. LLM-based Reranker(基于 LLM)
利用 LLM 的理解能力进行重排:
// rag/rerank.go
// LLMReranker 使用 LLM 进行重排
type LLMReranker struct {
apiURL string
apiKey string
model string
client *http.Client
}
// Rerank 使用 LLM 对检索结果进行重排
func (r *LLMReranker) Rerank(query string, documents []QueryResult) ([]QueryResult, error) {
// 构建重排提示
prompt := buildRerankPrompt(query, documents)
// 调用 LLM 获取分数
scores, err := r.callLLM(prompt)
if err != nil {
return documents, err
}
// 更新分数并重新排序
for i := range documents {
documents[i].Score = scores[i]
}
sort.Slice(documents, func(i, j int) bool {
return documents[i].Score > documents[j].Score
})
return documents, nil
}
// buildRerankPrompt 构建重排提示
func buildRerankPrompt(query string, documents []QueryResult) string {
var prompt strings.Builder
prompt.WriteString("你是一个文档检索重排系统。请根据用户查询,对以下文档进行相关性评分(0-1之间的小数)。\n\n")
prompt.WriteString(fmt.Sprintf("用户查询:%s\n\n", query))
prompt.WriteString("文档列表:\n")
for i, doc := range documents {
title := doc.Document.Metadata["title"].(string)
content := doc.Document.Content
if len(content) > 500 {
content = content[:500] + "..."
}
prompt.WriteString(fmt.Sprintf("[%d] 标题: %s\n内容: %s\n\n", i+1, title, content))
}
prompt.WriteString("请返回一个 JSON 数组,包含每个文档的相关性分数。\n")
prompt.WriteString("只返回 JSON 数组,例如:[0.95, 0.82, 0.65, 0.43, 0.21]")
return prompt.String()
}
2. Cohere Reranker(专业重排模型)
使用 Cohere 提供的专业重排 API:
// CohereReranker 使用 Cohere Rerank API 进行重排
type CohereReranker struct {
apiKey string
model string
client *http.Client
}
// Rerank 使用 Cohere API 对检索结果进行重排
func (r *CohereReranker) Rerank(query string, documents []QueryResult) ([]QueryResult, error) {
// 准备文档文本
documentsText := make([]string, len(documents))
for i, doc := range documents {
documentsText[i] = doc.Document.Content
}
// 调用 Cohere Rerank API
requestBody := map[string]interface{}{
"model": r.model, // "rerank-english-v3.0"
"query": query,
"documents": documentsText,
"top_n": len(documents),
}
// 发送请求到 https://api.cohere.ai/v1/rerank
// 返回带有 relevance_score 的结果
}
Reranker 对比
| 特性 | LLM Reranker | Cohere Reranker |
|---|---|---|
| 成本 | 按 token 计费 | 专门定价,较便宜 |
| 速度 | 较慢(生成式) | 较快(专用模型) |
| 质量 | 取决于 LLM | 专门优化,质量高 |
| 部署 | 复用现有 LLM | 需要 Cohere API |
| 适用 | 小规模/测试 | 生产环境 |
集成到 Retriever
// rag/retriever.go
type Retriever struct {
collectionManager *CollectionManager
collectionName string
defaultTopK int
reranker Reranker // 🆕 重排器
}
// Query 在集合中搜索相似文档
func (r *Retriever) Query(queryText string, topK int) ([]QueryResult, error) {
// 1. 向量检索(粗筛)
results, err := collection.Query(r.ctx, queryText, topK, nil, nil)
// 2. 如果启用了重排,进行精排
if r.reranker != nil {
reranked, err := r.reranker.Rerank(queryText, queryResults)
if err != nil {
// 重排失败不影响主流程
fmt.Printf("⚠️ 警告: 重排失败,使用原始结果: %v\n", err)
return queryResults, nil
}
return reranked, nil
}
return queryResults, nil
}
扩展知识:RAG 进阶技术
1. HyDE(Hypothetical Document Embeddings)
问题:用户查询通常很短,而文档通常很长,两者的向量空间不匹配。
查询向量: "登录失败怎么办" → [0.1, 0.2, ...] (短文本)
文档向量: "当用户登录失败时,通常有以下几种原因..." → [0.3, 0.4, ...] (长文本)
两者在向量空间中距离较远!
HyDE 的思路:先让 LLM 生成一个"假设的答案文档",再用这个假设文档去检索
┌─────────────────────────────────────────────────────────────┐
│ HyDE 工作流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户查询: "登录失败怎么办" │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ 1. LLM 生成假设文档 │ │
│ │ "登录失败通常是由于密码 │ │
│ │ 错误、账号被锁定或网络 │ │
│ │ 问题导致。解决方案包括..." │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ 2. 用假设文档进行向量检索 │ │
│ │ 假设文档向量 与 真实文档 │ │
│ │ 向量更相似! │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ 3. 返回真正相关的文档 │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
HyDE 的实现思路(伪代码):
func HyDERetrieve(query string) ([]Document, error) {
// 1. 生成假设文档
prompt := fmt.Sprintf("请写一段文字来回答这个问题:%s", query)
hypotheticalDoc, _ := llm.Generate(prompt)
// 2. 用假设文档进行检索
results, _ := vectorStore.Query(hypotheticalDoc, topK)
return results, nil
}
2. Query Expansion(查询扩展)
问题:用户查询可能措辞不当,遗漏关键词
解决方案:用 LLM 扩展查询
原始查询: "登录不了"
扩展后:
- "登录失败"
- "无法登录"
- "登录问题"
- "登录错误"
- "账号登录异常"
用多个扩展查询进行检索,合并结果
3. Multi-Query Retrieval(多查询检索)
从不同角度生成多个查询,获取更全面的结果:
func MultiQueryRetrieve(originalQuery string) ([]Document, error) {
// 1. 生成多角度查询
prompt := fmt.Sprintf(`
基于这个问题,从不同角度生成3个相关的搜索查询:
问题:%s
返回JSON数组格式
`, originalQuery)
queries := llm.Generate(prompt) // ["查询1", "查询2", "查询3"]
// 2. 对每个查询进行检索
var allResults []Document
for _, q := range queries {
results, _ := vectorStore.Query(q, topK)
allResults = append(allResults, results...)
}
// 3. 去重和排序
return deduplicate(allResults), nil
}
4. RAG Fusion
结合多种检索方法的结果,用 Reciprocal Rank Fusion (RRF) 算法合并排名:
┌─────────────────────────────────────────────────────────────┐
│ RAG Fusion │
├─────────────────────────────────────────────────────────────┤
│ │
│ 查询 ──┬──▶ 向量检索 ──▶ [Doc1, Doc3, Doc5] │
│ │ │
│ ├──▶ 关键词检索 ──▶ [Doc2, Doc1, Doc4] │
│ │ │
│ └──▶ 语义检索 ──▶ [Doc3, Doc2, Doc6] │
│ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ RRF 融合排序 │ │
│ │ score = Σ 1/(k+rank) │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ [Doc1, Doc3, Doc2, Doc5, ...] │
│ │
└─────────────────────────────────────────────────────────────┘
RAG 技术对比
| 技术 | 解决问题 | 复杂度 | 效果提升 |
|---|---|---|---|
| 基础 RAG | 知识扩展 | ⭐ | 基准 |
| Rerank | 排序准确性 | ⭐⭐ | +20% |
| HyDE | 查询-文档匹配 | ⭐⭐ | +15% |
| Query Expansion | 查询覆盖度 | ⭐⭐ | +10% |
| Multi-Query | 多角度召回 | ⭐⭐⭐ | +15% |
| RAG Fusion | 综合优化 | ⭐⭐⭐⭐ | +25% |
配置示例
Rerank 配置
// config/config.go
type ChromaDBConfig struct {
StoragePath string
Collection string
RAGEnabled bool
RAGTopK int
Rerank RerankConfig // 🆕 重排配置
}
type RerankConfig struct {
Enabled bool
Type string // "llm" 或 "cohere"
Model string
APIURL string
APIKey string
}
// 使用 LLM 重排的配置
config := &Config{
ChromaDB: ChromaDBConfig{
RAGEnabled: true,
RAGTopK: 20, // 粗筛取 20 个
Rerank: RerankConfig{
Enabled: true,
Type: "llm",
Model: "gpt-3.5-turbo",
APIURL: "https://api.openai.com/v1/chat/completions",
},
},
}
总结
本次更新解决了什么?
| 问题 | 解决方案 |
|---|---|
| 代码耦合 | 引入 langchaingo,模块化拆分 |
| 缺乏标准 | 使用 langchaingo 的标准接口 |
| 检索不准 | 添加 Rerank 重排机制 |
关键收获
- 框架的价值:langchaingo 提供统一抽象,简化开发
- 职责分离:LLM、Memory、Retriever 各司其职
- Rerank 重要:向量检索只是第一步,重排提升精度
Agent 进化路线
Level 1: 基础对话 ✅
↓
Level 2: 智能记忆 ✅
↓
Level 3: RAG 知识库 ✅
↓
Level 4: 框架重构 + Rerank ✅ ← 当前
↓
Level 5: 工具调用(Function Calling)
↓
Level 6: 多 Agent 协作
作者注:从"能用"到"好用"的关键在于两点:框架化和精细化。langchaingo 让代码更规范,Rerank 让检索更精准。这两步是 AI Agent 工程化的必经之路。
Happy Coding! 🚀
更多推荐



所有评论(0)