AgentFramework-零基础入门-第09章_实战项目2_文档问答系统
本文介绍了一个基于RAG(检索增强生成)技术的文档问答系统项目。该系统能够加载处理多种格式文档(TXT、MD、PDF),将内容向量化存储,并根据用户问题检索相关信息生成准确答案。项目采用C#/.NET8.0开发,包含文档加载器、文本分割器、嵌入生成器、向量存储和RAG代理等核心组件。文章详细说明了系统架构、功能需求、技术实现步骤(包括文档处理、向量检索和答案生成流程),并提供了完整的代码模板和测试

项目2:文档问答系统 - 需求文档
项目概述
在本项目中,你将创建一个基于 RAG(检索增强生成)技术的文档问答系统。该系统能够:
-
加载和处理多种格式的文档
-
将文档内容向量化并存储
-
根据用户问题检索相关内容
-
生成准确、有依据的答案
这个项目将深入实践 RAG 技术,这是当前 AI 应用中最重要的技术之一。
项目目标
通过完成这个项目,你将:
-
深入理解 RAG 的工作原理
-
掌握文档处理和向量化技术
-
学会使用向量数据库
-
实践信息检索和答案生成
-
了解如何评估和优化 RAG 系统
功能需求
1. 文档加载功能
-
支持多种文档格式:TXT、MD、PDF
-
能够批量加载文档
-
显示加载进度和结果
2. 文档处理功能
-
将文档分割成合适大小的块(chunks)
-
为每个文档块生成向量嵌入(embeddings)
-
将向量存储到向量数据库
3. 问答功能
-
接收用户的自然语言问题
-
从向量数据库检索相关文档块
-
基于检索到的内容生成答案
-
显示答案的来源文档
4. 文档管理功能
-
查看已加载的文档列表
-
删除指定文档
-
清空所有文档
-
显示文档统计信息
5. 答案质量优化
-
显示检索到的相关文档片段
-
提供答案的置信度评分
-
当找不到相关信息时,诚实地告知用户
技术需求
1. 使用的技术栈
-
Microsoft Agent Framework
-
Azure OpenAI(用于生成嵌入和答案)
-
向量数据库(使用内存向量存储)
-
C# / .NET 8.0
2. 核心组件
-
文档加载器:读取和解析文档
-
文本分割器:将文档分割成块
-
嵌入生成器:生成文本向量
-
向量存储:存储和检索向量
-
RAG 代理:协调整个问答流程
3. 数据处理
-
文档块大小:500-1000 字符
-
块之间的重叠:100-200 字符
-
检索数量:Top 3-5 个最相关的块
应用场景
场景1:技术文档问答
文档:Microsoft Agent Framework 官方文档
问题:如何创建一个带工具的代理?
答案:[基于文档内容生成详细答案]
来源:第3章 - 为代理添加工具功能
场景2:产品手册查询
文档:产品使用手册
问题:如何重置设备?
答案:[提供具体的重置步骤]
来源:第5章 - 故障排除
场景3:知识库问答
文档:公司内部知识库
问题:请假流程是什么?
答案:[详细的请假流程说明]
来源:人事制度文档
用户交互流程
1. 初始化阶段
系统:欢迎使用文档问答系统!
系统:请先加载文档。输入文档路径或使用示例文档。
用户:使用示例文档
系统:正在加载示例文档...
系统:✓ 已加载 3 个文档,共 150 个文档块
系统:现在可以开始提问了!
2. 问答阶段
用户:什么是 AI 代理?
系统:正在搜索相关信息...
系统:[生成的答案]
📚 参考来源:
1. 第01章_环境准备和基础概念/01_什么是AI代理.md
2. 第02章_创建第一个简单代理/01_代理的基本概念.md
还有其他问题吗?
3. 管理阶段
用户:查看文档列表
系统:已加载的文档:
1. 第01章_环境准备和基础概念/01_什么是AI代理.md (5 块)
2. 第02章_创建第一个简单代理/01_代理的基本概念.md (8 块)
3. 第03章_为代理添加工具功能/01_什么是工具.md (6 块)
总计:3 个文档,19 个文档块
评估标准
完成的项目应该满足:
-
✅ 能够正确加载和处理文档
-
✅ 检索结果准确相关
-
✅ 答案基于文档内容,不编造信息
-
✅ 能够引用答案来源
-
✅ 代码结构清晰,易于扩展
-
✅ 有适当的错误处理
技术挑战
1. 文档分割策略
-
如何确定合适的块大小?
-
如何处理跨块的信息?
-
如何保持上下文连贯性?
2. 检索质量
-
如何提高检索准确度?
-
如何处理语义相似但不相关的内容?
-
如何平衡检索数量和质量?
3. 答案生成
-
如何确保答案基于文档内容?
-
如何处理文档中没有的信息?
-
如何提高答案的可读性?
预计完成时间
-
初级学习者:6-8 小时
-
有编程基础:3-4 小时
-
有 RAG 经验:2-3 小时
扩展方向
完成基础功能后,可以尝试:
-
支持更多文档格式(Word、Excel)
-
实现文档更新和增量索引
-
添加多语言支持
-
实现对话式问答(多轮对话)
-
添加答案评分和反馈机制
-
集成持久化向量数据库(如 Qdrant、Milvus)
项目2:文档问答系统 - 设计指导
系统架构
整体架构图
┌─────────────────────────────────────────────────┐
│ 用户界面层 │
│ (控制台交互 / 命令处理) │
└────────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ RAG 代理层 │
│ - 理解用户问题 │
│ - 协调检索和生成 │
│ - 格式化输出 │
└────────┬────────────────────┬────────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ 检索模块 │ │ 生成模块 │
│ - 向量检索 │ │ - 答案生成 │
│ - 相似度计算 │ │ - 来源引用 │
└────────┬─────────┘ └──────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 向量存储层 │
│ - 文档向量存储 │
│ - 相似度搜索 │
│ - 元数据管理 │
└────────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 文档处理层 │
│ - 文档加载 │
│ - 文本分割 │
│ - 向量生成 │
└─────────────────────────────────────────────────┘
核心组件设计
1. 文档加载器 (DocumentLoader)
职责:读取和解析不同格式的文档
public interface IDocumentLoader
{
/// <summary>
/// 加载文档
/// </summary>
Task<Document> LoadAsync(string filePath);
/// <summary>
/// 批量加载文档
/// </summary>
Task<List<Document>> LoadBatchAsync(string[] filePaths);
/// <summary>
/// 支持的文件格式
/// </summary>
string[] SupportedFormats { get; }
}
实现要点:
-
根据文件扩展名选择合适的解析器
-
提取文档元数据(文件名、路径、大小等)
-
处理编码问题
-
错误处理和日志记录
2. 文本分割器 (TextSplitter)
职责:将长文档分割成合适大小的块
public class TextSplitter
{
private readonly int _chunkSize; // 块大小
private readonly int _chunkOverlap; // 重叠大小
/// <summary>
/// 分割文本
/// </summary>
public List<DocumentChunk> Split(Document document)
{
// 实现分割逻辑
}
}
分割策略:
-
固定大小分割
-
优点:简单、可预测
-
缺点:可能破坏语义完整性
-
-
按段落分割
-
优点:保持语义完整
-
缺点:块大小不均匀
-
-
混合策略(推荐)
-
优先按段落分割
-
超大段落再按固定大小分割
-
保持一定的重叠
-
重叠的作用:
块1: [AAAA BBBB CCCC]
块2: [CCCC DDDD EEEE]
块3: [EEEE FFFF GGGG]
重叠部分确保跨块信息不会丢失。
3. 嵌入生成器 (EmbeddingGenerator)
职责:将文本转换为向量表示
public class EmbeddingGenerator
{
private readonly IEmbeddingGenerator<string, Embedding<float>> _generator;
/// <summary>
/// 生成单个文本的嵌入
/// </summary>
public async Task<float[]> GenerateAsync(string text)
{
var embeddings = await _generator.GenerateAsync([text]);
return embeddings[0].Vector.ToArray();
}
/// <summary>
/// 批量生成嵌入
/// </summary>
public async Task<List<float[]>> GenerateBatchAsync(List<string> texts)
{
var embeddings = await _generator.GenerateAsync(texts);
return embeddings.Select(e => e.Vector.ToArray()).ToList();
}
}
注意事项:
-
使用与检索相同的嵌入模型
-
批量处理提高效率
-
处理超长文本(截断或分段)
-
缓存常用嵌入
4. 向量存储 (VectorStore)
职责:存储和检索文档向量
public class InMemoryVectorStore
{
private readonly List<VectorEntry> _vectors = new();
/// <summary>
/// 添加向量
/// </summary>
public void Add(string id, float[] vector, Dictionary<string, object> metadata)
{
_vectors.Add(new VectorEntry
{
Id = id,
Vector = vector,
Metadata = metadata
});
}
/// <summary>
/// 相似度搜索
/// </summary>
public List<SearchResult> Search(float[] queryVector, int topK)
{
// 计算余弦相似度
var results = _vectors
.Select(entry => new SearchResult
{
Id = entry.Id,
Score = CosineSimilarity(queryVector, entry.Vector),
Metadata = entry.Metadata
})
.OrderByDescending(r => r.Score)
.Take(topK)
.ToList();
return results;
}
/// <summary>
/// 计算余弦相似度
/// </summary>
private float CosineSimilarity(float[] a, float[] b)
{
float dot = 0, magA = 0, magB = 0;
for (int i = 0; i < a.Length; i++)
{
dot += a[i] * b[i];
magA += a[i] * a[i];
magB += b[i] * b[i];
}
return dot / (MathF.Sqrt(magA) * MathF.Sqrt(magB));
}
}
5. RAG 代理 (RAGAgent)
职责:协调整个问答流程
public class RAGAgent
{
private readonly IChatClient _chatClient;
private readonly EmbeddingGenerator _embeddingGenerator;
private readonly InMemoryVectorStore _vectorStore;
/// <summary>
/// 回答问题
/// </summary>
public async Task<RAGResponse> AnswerAsync(string question)
{
// 1. 生成问题的嵌入
var questionEmbedding = await _embeddingGenerator.GenerateAsync(question);
// 2. 检索相关文档
var searchResults = _vectorStore.Search(questionEmbedding, topK: 3);
// 3. 构建上下文
var context = BuildContext(searchResults);
// 4. 生成答案
var answer = await GenerateAnswerAsync(question, context);
// 5. 返回结果
return new RAGResponse
{
Answer = answer,
Sources = searchResults.Select(r => r.Metadata["source"].ToString()).ToList(),
RelevanceScores = searchResults.Select(r => r.Score).ToList()
};
}
private string BuildContext(List<SearchResult> results)
{
var context = new StringBuilder();
context.AppendLine("以下是相关的文档内容:\n");
for (int i = 0; i < results.Count; i++)
{
context.AppendLine($"[文档 {i + 1}]");
context.AppendLine($"来源:{results[i].Metadata["source"]}");
context.AppendLine($"内容:{results[i].Metadata["content"]}");
context.AppendLine();
}
return context.ToString();
}
private async Task<string> GenerateAnswerAsync(string question, string context)
{
var prompt = $@"
基于以下文档内容回答问题。
{context}
问题:{question}
要求:
1. 只基于提供的文档内容回答
2. 如果文档中没有相关信息,请明确说明
3. 回答要准确、简洁、易懂
4. 可以引用文档编号
答案:";
var response = await _chatClient.CompleteAsync(prompt);
return response.Message.Text ?? "无法生成答案";
}
}
数据模型设计
Document(文档)
public class Document
{
public string Id { get; set; } // 文档ID
public string FilePath { get; set; } // 文件路径
public string FileName { get; set; } // 文件名
public string Content { get; set; } // 文档内容
public DateTime LoadedAt { get; set; } // 加载时间
public long FileSize { get; set; } // 文件大小
public Dictionary<string, object> Metadata { get; set; } // 元数据
}
DocumentChunk(文档块)
public class DocumentChunk
{
public string Id { get; set; } // 块ID
public string DocumentId { get; set; } // 所属文档ID
public string Content { get; set; } // 块内容
public int ChunkIndex { get; set; } // 块索引
public int StartPosition { get; set; } // 起始位置
public int EndPosition { get; set; } // 结束位置
public Dictionary<string, object> Metadata { get; set; } // 元数据
}
VectorEntry(向量条目)
public class VectorEntry
{
public string Id { get; set; } // 条目ID
public float[] Vector { get; set; } // 向量
public Dictionary<string, object> Metadata { get; set; } // 元数据
}
SearchResult(搜索结果)
public class SearchResult
{
public string Id { get; set; } // 结果ID
public float Score { get; set; } // 相似度分数
public Dictionary<string, object> Metadata { get; set; } // 元数据
}
RAGResponse(RAG响应)
public class RAGResponse
{
public string Answer { get; set; } // 答案
public List<string> Sources { get; set; } // 来源列表
public List<float> RelevanceScores { get; set; } // 相关度分数
public int RetrievedChunks { get; set; } // 检索到的块数
}
工作流程
1. 文档加载流程
用户输入文档路径
↓
验证文件是否存在
↓
根据扩展名选择加载器
↓
读取文件内容
↓
提取元数据
↓
创建 Document 对象
↓
返回加载结果
2. 文档索引流程
接收 Document 对象
↓
使用 TextSplitter 分割文档
↓
为每个块生成嵌入向量
↓
将向量和元数据存储到 VectorStore
↓
更新文档统计信息
↓
返回索引结果
3. 问答流程
接收用户问题
↓
生成问题的嵌入向量
↓
在 VectorStore 中检索相似向量
↓
获取对应的文档块
↓
构建上下文 Prompt
↓
调用 LLM 生成答案
↓
格式化答案和来源
↓
返回给用户
关键技术点
1. 文本分割策略
推荐参数:
-
块大小:500-1000 字符
-
重叠大小:100-200 字符(块大小的 10-20%)
分割代码示例:
public List<DocumentChunk> Split(string text, int chunkSize, int overlap)
{
var chunks = new List<DocumentChunk>();
int position = 0;
int index = 0;
while (position < text.Length)
{
int length = Math.Min(chunkSize, text.Length - position);
string chunkText = text.Substring(position, length);
chunks.Add(new DocumentChunk
{
Content = chunkText,
ChunkIndex = index,
StartPosition = position,
EndPosition = position + length
});
position += chunkSize - overlap;
index++;
}
return chunks;
}
2. 相似度计算
余弦相似度:
similarity = (A · B) / (||A|| × ||B||)
特点:
-
范围:-1 到 1(通常是 0 到 1)
-
值越大表示越相似
-
不受向量长度影响
3. Prompt 工程
好的 RAG Prompt 应该:
-
明确指示基于提供的内容回答
-
要求引用来源
-
处理信息不足的情况
-
控制答案格式和长度
示例:
你是一个专业的文档问答助手。请基于以下文档内容回答用户的问题。
文档内容:
[检索到的文档块]
用户问题:
[用户的问题]
回答要求:
1. 只基于提供的文档内容回答,不要编造信息
2. 如果文档中没有相关信息,请明确说明"文档中没有找到相关信息"
3. 回答要准确、简洁、易懂
4. 可以引用文档编号说明信息来源
5. 如果需要,可以整合多个文档的信息
你的回答:
性能优化
1. 批量处理
-
批量生成嵌入向量
-
批量插入向量存储
-
减少 API 调用次数
2. 缓存策略
-
缓存文档嵌入
-
缓存常见问题的答案
-
使用内存缓存提高速度
3. 异步处理
-
使用 async/await
-
并行处理多个文档
-
异步加载和索引
错误处理
1. 文件加载错误
-
文件不存在
-
文件格式不支持
-
文件编码问题
-
文件过大
2. API 调用错误
-
网络超时
-
API 限流
-
认证失败
-
配额不足
3. 数据处理错误
-
文本分割失败
-
向量生成失败
-
检索失败
测试策略
1. 单元测试
-
测试文本分割逻辑
-
测试相似度计算
-
测试文档加载
2. 集成测试
-
测试完整的问答流程
-
测试多文档场景
-
测试边界情况
3. 质量评估
-
答案准确性
-
检索相关性
-
响应时间
项目2:文档问答系统 - 项目模板代码
项目结构
DocumentQASystem/
├── DocumentQASystem.csproj
├── Program.cs // 主程序
├── Models/
│ ├── Document.cs // 文档模型
│ ├── DocumentChunk.cs // 文档块模型
│ ├── VectorEntry.cs // 向量条目模型
│ ├── SearchResult.cs // 搜索结果模型
│ └── RAGResponse.cs // RAG响应模型
├── Services/
│ ├── DocumentLoader.cs // 文档加载器
│ ├── TextSplitter.cs // 文本分割器
│ ├── EmbeddingService.cs // 嵌入服务
│ ├── VectorStore.cs // 向量存储
│ └── RAGService.cs // RAG服务
└── SampleDocuments/ // 示例文档目录
├── sample1.txt
├── sample2.txt
└── sample3.txt
1. 项目文件
DocumentQASystem.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.0.1-preview.1.24570.5" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.0.1-preview.1.24570.5" />
</ItemGroup>
</Project>
2. 数据模型
Models/Document.cs
namespace DocumentQASystem.Models;
/// <summary>
/// 文档模型
/// </summary>
public class Document
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string FilePath { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime LoadedAt { get; set; } = DateTime.Now;
public long FileSize { get; set; }
public Dictionary<string, object> Metadata { get; set; } = new();
}
Models/DocumentChunk.cs
namespace DocumentQASystem.Models;
/// <summary>
/// 文档块模型
/// </summary>
public class DocumentChunk
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string DocumentId { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public int ChunkIndex { get; set; }
public int StartPosition { get; set; }
public int EndPosition { get; set; }
public Dictionary<string, object> Metadata { get; set; } = new();
}
Models/VectorEntry.cs
namespace DocumentQASystem.Models;
/// <summary>
/// 向量条目模型
/// </summary>
public class VectorEntry
{
public string Id { get; set; } = string.Empty;
public float[] Vector { get; set; } = Array.Empty<float>();
public Dictionary<string, object> Metadata { get; set; } = new();
}
Models/SearchResult.cs
namespace DocumentQASystem.Models;
/// <summary>
/// 搜索结果模型
/// </summary>
public class SearchResult
{
public string Id { get; set; } = string.Empty;
public float Score { get; set; }
public Dictionary<string, object> Metadata { get; set; } = new();
}
Models/RAGResponse.cs
namespace DocumentQASystem.Models;
/// <summary>
/// RAG响应模型
/// </summary>
public class RAGResponse
{
public string Answer { get; set; } = string.Empty;
public List<string> Sources { get; set; } = new();
public List<float> RelevanceScores { get; set; } = new();
public int RetrievedChunks { get; set; }
}
3. 服务类(待实现)
Services/DocumentLoader.cs
using DocumentQASystem.Models;
namespace DocumentQASystem.Services;
/// <summary>
/// 文档加载器
/// </summary>
public class DocumentLoader
{
// TODO: 实现文档加载功能
public async Task<Document> LoadAsync(string filePath)
{
// 提示:
// 1. 检查文件是否存在
// 2. 读取文件内容
// 3. 创建 Document 对象
// 4. 填充元数据
throw new NotImplementedException("请实现文档加载功能");
}
// TODO: 实现批量加载功能
public async Task<List<Document>> LoadBatchAsync(string[] filePaths)
{
// 提示:
// 1. 遍历文件路径
// 2. 调用 LoadAsync 加载每个文件
// 3. 收集结果
// 4. 处理错误
throw new NotImplementedException("请实现批量加载功能");
}
}
Services/TextSplitter.cs
using DocumentQASystem.Models;
namespace DocumentQASystem.Services;
/// <summary>
/// 文本分割器
/// </summary>
public class TextSplitter
{
private readonly int _chunkSize;
private readonly int _chunkOverlap;
public TextSplitter(int chunkSize = 500, int chunkOverlap = 100)
{
_chunkSize = chunkSize;
_chunkOverlap = chunkOverlap;
}
// TODO: 实现文本分割功能
public List<DocumentChunk> Split(Document document)
{
// 提示:
// 1. 获取文档内容
// 2. 按照 chunkSize 和 chunkOverlap 分割
// 3. 创建 DocumentChunk 对象
// 4. 填充元数据
throw new NotImplementedException("请实现文本分割功能");
}
}
Services/EmbeddingService.cs
using Microsoft.Extensions.AI;
namespace DocumentQASystem.Services;
/// <summary>
/// 嵌入服务
/// </summary>
public class EmbeddingService
{
private readonly IEmbeddingGenerator<string, Embedding<float>> _generator;
public EmbeddingService(IEmbeddingGenerator<string, Embedding<float>> generator)
{
_generator = generator;
}
// TODO: 实现单个文本嵌入生成
public async Task<float[]> GenerateAsync(string text)
{
// 提示:
// 1. 调用 _generator.GenerateAsync
// 2. 提取向量
// 3. 转换为 float[]
throw new NotImplementedException("请实现嵌入生成功能");
}
// TODO: 实现批量嵌入生成
public async Task<List<float[]>> GenerateBatchAsync(List<string> texts)
{
// 提示:
// 1. 批量调用 _generator.GenerateAsync
// 2. 提取所有向量
// 3. 转换为 List<float[]>
throw new NotImplementedException("请实现批量嵌入生成功能");
}
}
Services/VectorStore.cs
using DocumentQASystem.Models;
namespace DocumentQASystem.Services;
/// <summary>
/// 向量存储(内存实现)
/// </summary>
public class InMemoryVectorStore
{
private readonly List<VectorEntry> _vectors = new();
// TODO: 实现添加向量功能
public void Add(string id, float[] vector, Dictionary<string, object> metadata)
{
// 提示:
// 1. 创建 VectorEntry 对象
// 2. 添加到 _vectors 列表
throw new NotImplementedException("请实现添加向量功能");
}
// TODO: 实现相似度搜索功能
public List<SearchResult> Search(float[] queryVector, int topK)
{
// 提示:
// 1. 遍历所有向量
// 2. 计算余弦相似度
// 3. 排序并返回 Top K
throw new NotImplementedException("请实现相似度搜索功能");
}
// TODO: 实现余弦相似度计算
private float CosineSimilarity(float[] a, float[] b)
{
// 提示:
// 1. 计算点积
// 2. 计算向量长度
// 3. 返回 dot / (||a|| * ||b||)
throw new NotImplementedException("请实现余弦相似度计算");
}
// 获取统计信息
public int Count => _vectors.Count;
// 清空存储
public void Clear() => _vectors.Clear();
}
Services/RAGService.cs
using Microsoft.Extensions.AI;
using DocumentQASystem.Models;
namespace DocumentQASystem.Services;
/// <summary>
/// RAG服务
/// </summary>
public class RAGService
{
private readonly IChatClient _chatClient;
private readonly EmbeddingService _embeddingService;
private readonly InMemoryVectorStore _vectorStore;
public RAGService(
IChatClient chatClient,
EmbeddingService embeddingService,
InMemoryVectorStore vectorStore)
{
_chatClient = chatClient;
_embeddingService = embeddingService;
_vectorStore = vectorStore;
}
// TODO: 实现问答功能
public async Task<RAGResponse> AnswerAsync(string question, int topK = 3)
{
// 提示:
// 1. 生成问题的嵌入向量
// 2. 在向量存储中检索相关文档
// 3. 构建上下文
// 4. 生成答案
// 5. 返回 RAGResponse
throw new NotImplementedException("请实现问答功能");
}
// TODO: 实现上下文构建
private string BuildContext(List<SearchResult> results)
{
// 提示:
// 1. 遍历搜索结果
// 2. 格式化每个文档块
// 3. 组合成完整的上下文
throw new NotImplementedException("请实现上下文构建");
}
// TODO: 实现答案生成
private async Task<string> GenerateAnswerAsync(string question, string context)
{
// 提示:
// 1. 构建 Prompt
// 2. 调用 _chatClient.CompleteAsync
// 3. 提取答案文本
throw new NotImplementedException("请实现答案生成");
}
}
4. 主程序(待实现)
Program.cs
using Azure.AI.OpenAI;
using Microsoft.Extensions.AI;
using DocumentQASystem.Services;
using DocumentQASystem.Models;
using System.Text;
// 设置控制台编码
Console.OutputEncoding = Encoding.UTF8;
// TODO: 配置 Azure OpenAI
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? "";
string apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY") ?? "";
string chatDeployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_CHAT_DEPLOYMENT") ?? "gpt-4";
string embeddingDeployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT") ?? "text-embedding-ada-002";
Console.WriteLine("╔═══════════════════════════════════════╗");
Console.WriteLine("║ ║");
Console.WriteLine("║ 📚 文档问答系统 v1.0 ║");
Console.WriteLine("║ ║");
Console.WriteLine("╚═══════════════════════════════════════╝");
Console.WriteLine();
try
{
// TODO: 创建 OpenAI 客户端
// 提示:需要创建聊天客户端和嵌入客户端
// TODO: 创建服务实例
// 提示:DocumentLoader, TextSplitter, EmbeddingService, VectorStore, RAGService
// TODO: 加载示例文档或让用户选择文档
// TODO: 处理文档(分割、生成嵌入、存储)
// TODO: 实现命令循环
// 支持的命令:
// - ask <问题>: 提问
// - load <路径>: 加载文档
// - list: 列出已加载的文档
// - clear: 清空文档
// - help: 显示帮助
// - exit: 退出
Console.WriteLine("\n感谢使用文档问答系统!");
}
catch (Exception ex)
{
Console.WriteLine($"\n发生错误:{ex.Message}");
}
5. 示例文档
SampleDocuments/sample1.txt
什么是AI代理?
AI代理(AI Agent)是一个能够感知环境、做出决策并采取行动的智能系统。
它可以理解自然语言,使用工具完成任务,并与用户进行交互。
AI代理的核心特征:
1. 自主性:能够独立做出决策
2. 反应性:能够感知环境变化并做出响应
3. 主动性:能够主动采取行动实现目标
4. 社交性:能够与其他代理或人类交互
常见的AI代理应用:
- 智能客服助手
- 个人助理
- 自动化工作流
- 游戏NPC
SampleDocuments/sample2.txt
如何创建AI代理?
使用 Microsoft Agent Framework 创建代理非常简单:
1. 安装必要的包
2. 配置 Azure OpenAI 连接
3. 创建 ChatCompletionAgent 实例
4. 设置代理的指令(Instructions)
5. 调用代理处理用户输入
示例代码:
var agent = new ChatCompletionAgent
{
Name = "我的代理",
Instructions = "你是一个友好的助手",
Kernel = new Kernel()
};
SampleDocuments/sample3.txt
什么是RAG?
RAG(Retrieval-Augmented Generation,检索增强生成)是一种结合信息检索和文本生成的技术。
RAG的工作流程:
1. 将文档分割成小块
2. 为每个块生成向量嵌入
3. 存储到向量数据库
4. 接收用户问题
5. 检索相关文档块
6. 基于检索到的内容生成答案
RAG的优势:
- 答案基于真实文档,更准确
- 可以引用信息来源
- 不需要重新训练模型
- 易于更新知识库
使用说明
1. 创建项目
dotnet new console -n DocumentQASystem
cd DocumentQASystem
2. 添加包引用
dotnet add package Azure.AI.OpenAI
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI
3. 创建目录结构
mkdir Models Services SampleDocuments
4. 复制代码文件
将上面的代码复制到对应的文件中。
5. 创建示例文档
将示例文档内容保存到 SampleDocuments 目录。
6. 配置环境变量
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
$env:AZURE_OPENAI_KEY="your-api-key"
$env:AZURE_OPENAI_CHAT_DEPLOYMENT="gpt-4"
$env:AZURE_OPENAI_EMBEDDING_DEPLOYMENT="text-embedding-ada-002"
7. 实现功能
按照 TODO 提示,逐步实现各个功能。
项目2:文档问答系统 - 分步骤实现指南
本指南将带你一步步完成文档问答系统项目。
实现顺序
建议按以下顺序实现:
-
文档加载器 → 2. 文本分割器 → 3. 向量存储 → 4. 嵌入服务 → 5. RAG服务 → 6. 主程序
第一步:实现文档加载器
目标
实现 DocumentLoader 类,能够加载文本文件。
实现代码
using DocumentQASystem.Models;
namespace DocumentQASystem.Services;
public class DocumentLoader
{
public async Task<Document> LoadAsync(string filePath)
{
try
{
// 检查文件是否存在
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"文件不存在:{filePath}");
}
// 读取文件内容
string content = await File.ReadAllTextAsync(filePath);
// 获取文件信息
var fileInfo = new FileInfo(filePath);
// 创建文档对象
var document = new Document
{
FilePath = filePath,
FileName = fileInfo.Name,
Content = content,
LoadedAt = DateTime.Now,
FileSize = fileInfo.Length,
Metadata = new Dictionary<string, object>
{
["extension"] = fileInfo.Extension,
["directory"] = fileInfo.DirectoryName ?? ""
}
};
return document;
}
catch (Exception ex)
{
throw new Exception($"加载文档失败:{ex.Message}", ex);
}
}
public async Task<List<Document>> LoadBatchAsync(string[] filePaths)
{
var documents = new List<Document>();
var errors = new List<string>();
foreach (var filePath in filePaths)
{
try
{
var document = await LoadAsync(filePath);
documents.Add(document);
Console.WriteLine($"✓ 已加载:{Path.GetFileName(filePath)}");
}
catch (Exception ex)
{
errors.Add($"✗ {Path.GetFileName(filePath)}: {ex.Message}");
}
}
// 显示错误
if (errors.Any())
{
Console.WriteLine("\n加载失败的文件:");
foreach (var error in errors)
{
Console.WriteLine($" {error}");
}
}
return documents;
}
}
第二步:实现文本分割器
目标
实现 TextSplitter 类,将文档分割成合适大小的块。
实现代码
using DocumentQASystem.Models;
namespace DocumentQASystem.Services;
public class TextSplitter
{
private readonly int _chunkSize;
private readonly int _chunkOverlap;
public TextSplitter(int chunkSize = 500, int chunkOverlap = 100)
{
_chunkSize = chunkSize;
_chunkOverlap = chunkOverlap;
}
public List<DocumentChunk> Split(Document document)
{
var chunks = new List<DocumentChunk>();
string text = document.Content;
if (string.IsNullOrWhiteSpace(text))
{
return chunks;
}
int position = 0;
int index = 0;
while (position < text.Length)
{
// 计算当前块的长度
int length = Math.Min(_chunkSize, text.Length - position);
// 提取文本块
string chunkText = text.Substring(position, length);
// 创建文档块
var chunk = new DocumentChunk
{
DocumentId = document.Id,
Content = chunkText.Trim(),
ChunkIndex = index,
StartPosition = position,
EndPosition = position + length,
Metadata = new Dictionary<string, object>
{
["document_id"] = document.Id,
["document_name"] = document.FileName,
["chunk_index"] = index,
["source"] = document.FileName
}
};
chunks.Add(chunk);
// 移动位置(考虑重叠)
position += _chunkSize - _chunkOverlap;
index++;
}
return chunks;
}
}
第三步:实现向量存储
目标
实现 InMemoryVectorStore 类,存储和检索向量。
实现代码
using DocumentQASystem.Models;
namespace DocumentQASystem.Services;
public class InMemoryVectorStore
{
private readonly List<VectorEntry> _vectors = new();
public void Add(string id, float[] vector, Dictionary<string, object> metadata)
{
_vectors.Add(new VectorEntry
{
Id = id,
Vector = vector,
Metadata = metadata
});
}
public List<SearchResult> Search(float[] queryVector, int topK)
{
if (_vectors.Count == 0)
{
return new List<SearchResult>();
}
// 计算所有向量的相似度
var results = _vectors
.Select(entry => new SearchResult
{
Id = entry.Id,
Score = CosineSimilarity(queryVector, entry.Vector),
Metadata = entry.Metadata
})
.OrderByDescending(r => r.Score)
.Take(topK)
.ToList();
return results;
}
private float CosineSimilarity(float[] a, float[] b)
{
if (a.Length != b.Length)
{
throw new ArgumentException("向量维度不匹配");
}
float dot = 0;
float magA = 0;
float magB = 0;
for (int i = 0; i < a.Length; i++)
{
dot += a[i] * b[i];
magA += a[i] * a[i];
magB += b[i] * b[i];
}
float magnitude = MathF.Sqrt(magA) * MathF.Sqrt(magB);
if (magnitude == 0)
{
return 0;
}
return dot / magnitude;
}
public int Count => _vectors.Count;
public void Clear() => _vectors.Clear();
}
第四步:实现嵌入服务
目标
实现 EmbeddingService 类,生成文本的向量嵌入。
实现代码
using Microsoft.Extensions.AI;
namespace DocumentQASystem.Services;
public class EmbeddingService
{
private readonly IEmbeddingGenerator<string, Embedding<float>> _generator;
public EmbeddingService(IEmbeddingGenerator<string, Embedding<float>> generator)
{
_generator = generator;
}
public async Task<float[]> GenerateAsync(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException("文本不能为空");
}
var embeddings = await _generator.GenerateAsync([text]);
return embeddings[0].Vector.ToArray();
}
public async Task<List<float[]>> GenerateBatchAsync(List<string> texts)
{
if (texts == null || texts.Count == 0)
{
return new List<float[]>();
}
var embeddings = await _generator.GenerateAsync(texts);
return embeddings.Select(e => e.Vector.ToArray()).ToList();
}
}
第五步:实现RAG服务
目标
实现 RAGService 类,协调整个问答流程。
实现代码
using Microsoft.Extensions.AI;
using DocumentQASystem.Models;
using System.Text;
namespace DocumentQASystem.Services;
public class RAGService
{
private readonly IChatClient _chatClient;
private readonly EmbeddingService _embeddingService;
private readonly InMemoryVectorStore _vectorStore;
public RAGService(
IChatClient chatClient,
EmbeddingService embeddingService,
InMemoryVectorStore vectorStore)
{
_chatClient = chatClient;
_embeddingService = embeddingService;
_vectorStore = vectorStore;
}
public async Task<RAGResponse> AnswerAsync(string question, int topK = 3)
{
try
{
// 1. 生成问题的嵌入
var questionEmbedding = await _embeddingService.GenerateAsync(question);
// 2. 检索相关文档
var searchResults = _vectorStore.Search(questionEmbedding, topK);
if (searchResults.Count == 0)
{
return new RAGResponse
{
Answer = "抱歉,我在文档中没有找到相关信息。请尝试换个方式提问,或者加载更多文档。",
Sources = new List<string>(),
RelevanceScores = new List<float>(),
RetrievedChunks = 0
};
}
// 3. 构建上下文
var context = BuildContext(searchResults);
// 4. 生成答案
var answer = await GenerateAnswerAsync(question, context);
// 5. 返回结果
return new RAGResponse
{
Answer = answer,
Sources = searchResults
.Select(r => r.Metadata["source"]?.ToString() ?? "未知来源")
.Distinct()
.ToList(),
RelevanceScores = searchResults.Select(r => r.Score).ToList(),
RetrievedChunks = searchResults.Count
};
}
catch (Exception ex)
{
return new RAGResponse
{
Answer = $"生成答案时发生错误:{ex.Message}",
Sources = new List<string>(),
RelevanceScores = new List<float>(),
RetrievedChunks = 0
};
}
}
private string BuildContext(List<SearchResult> results)
{
var context = new StringBuilder();
context.AppendLine("以下是相关的文档内容:\n");
for (int i = 0; i < results.Count; i++)
{
context.AppendLine($"[文档片段 {i + 1}]");
context.AppendLine($"来源:{results[i].Metadata["source"]}");
context.AppendLine($"相关度:{results[i].Score:F2}");
context.AppendLine($"内容:{results[i].Metadata["content"]}");
context.AppendLine();
}
return context.ToString();
}
private async Task<string> GenerateAnswerAsync(string question, string context)
{
var prompt = $@"你是一个专业的文档问答助手。请基于以下文档内容回答用户的问题。
{context}
用户问题:{question}
回答要求:
1. 只基于提供的文档内容回答,不要编造信息
2. 如果文档中没有相关信息,请明确说明
3. 回答要准确、简洁、易懂
4. 可以引用文档片段编号说明信息来源
5. 如果需要,可以整合多个文档片段的信息
你的回答:";
var response = await _chatClient.CompleteAsync(prompt);
return response.Message.Text ?? "无法生成答案";
}
}
第六步:实现主程序
目标
完成 Program.cs,整合所有组件。
实现代码
using Azure.AI.OpenAI;
using Microsoft.Extensions.AI;
using DocumentQASystem.Services;
using DocumentQASystem.Models;
using System.Text;
Console.OutputEncoding = Encoding.UTF8;
// 配置
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? "";
string apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY") ?? "";
string chatDeployment = "gpt-4";
string embeddingDeployment = "text-embedding-ada-002";
Console.WriteLine("╔═══════════════════════════════════════╗");
Console.WriteLine("║ 📚 文档问答系统 v1.0 ║");
Console.WriteLine("╚═══════════════════════════════════════╝\n");
try
{
// 创建客户端
var azureClient = new AzureOpenAIClient(
new Uri(endpoint),
new System.ClientModel.ApiKeyCredential(apiKey));
var chatClient = azureClient.AsChatClient(chatDeployment);
var embeddingClient = azureClient.AsEmbeddingGenerator(embeddingDeployment);
// 创建服务
var documentLoader = new DocumentLoader();
var textSplitter = new TextSplitter(chunkSize: 500, chunkOverlap: 100);
var embeddingService = new EmbeddingService(embeddingClient);
var vectorStore = new InMemoryVectorStore();
var ragService = new RAGService(chatClient, embeddingService, vectorStore);
Console.WriteLine("✓ 系统初始化完成\n");
// 加载示例文档
Console.WriteLine("正在加载示例文档...");
var sampleFiles = Directory.GetFiles("SampleDocuments", "*.txt");
var documents = await documentLoader.LoadBatchAsync(sampleFiles);
if (documents.Count == 0)
{
Console.WriteLine("⚠️ 没有加载任何文档");
return;
}
// 处理文档
Console.WriteLine("\n正在处理文档...");
int totalChunks = 0;
foreach (var doc in documents)
{
// 分割文档
var chunks = textSplitter.Split(doc);
totalChunks += chunks.Count;
// 生成嵌入并存储
foreach (var chunk in chunks)
{
var embedding = await embeddingService.GenerateAsync(chunk.Content);
chunk.Metadata["content"] = chunk.Content;
vectorStore.Add(chunk.Id, embedding, chunk.Metadata);
}
Console.WriteLine($" ✓ {doc.FileName}: {chunks.Count} 个文档块");
}
Console.WriteLine($"\n✓ 处理完成!共 {documents.Count} 个文档,{totalChunks} 个文档块");
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("现在可以开始提问了!");
Console.WriteLine("输入 'help' 查看帮助,输入 'exit' 退出");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 命令循环
while (true)
{
Console.Write("💬 您的问题: ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
continue;
if (input.Equals("exit", StringComparison.OrdinalIgnoreCase))
break;
if (input.Equals("help", StringComparison.OrdinalIgnoreCase))
{
ShowHelp();
continue;
}
// 回答问题
Console.WriteLine("\n🔍 正在搜索相关信息...\n");
var response = await ragService.AnswerAsync(input);
Console.WriteLine("📝 答案:");
Console.WriteLine(response.Answer);
Console.WriteLine();
if (response.Sources.Any())
{
Console.WriteLine("📚 参考来源:");
for (int i = 0; i < response.Sources.Count; i++)
{
Console.WriteLine($" {i + 1}. {response.Sources[i]} (相关度: {response.RelevanceScores[i]:F2})");
}
Console.WriteLine();
}
}
Console.WriteLine("\n感谢使用文档问答系统!");
}
catch (Exception ex)
{
Console.WriteLine($"\n❌ 错误:{ex.Message}");
}
static void ShowHelp()
{
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("📖 帮助信息");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("直接输入问题即可获得答案");
Console.WriteLine("命令:");
Console.WriteLine(" help - 显示此帮助");
Console.WriteLine(" exit - 退出程序");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
测试场景
场景1:基础问答
您的问题: 什么是AI代理?
答案: [基于文档生成的答案]
来源: sample1.txt
场景2:技术问题
您的问题: 如何创建AI代理?
答案: [包含代码示例的答案]
来源: sample2.txt
场景3:概念解释
您的问题: RAG是什么?
答案: [详细的RAG解释]
来源: sample3.txt
常见问题
问题1:检索结果不相关
解决方案:
-
调整 chunk size 和 overlap
-
增加检索数量(topK)
-
改进文档质量
问题2:答案不准确
解决方案:
-
优化 Prompt
-
提高检索质量
-
增加更多相关文档
问题3:性能慢
解决方案:
-
使用批量嵌入生成
-
缓存常见问题
-
优化向量搜索算法
项目2:文档问答系统 - 参考实现
项目总结
恭喜你完成了文档问答系统项目!🎉
实现的功能
✅ 文档加载和解析 ✅ 文本分割和向量化 ✅ 向量存储和检索 ✅ 基于RAG的问答 ✅ 来源引用和相关度评分
核心技术
-
RAG技术:检索增强生成
-
向量嵌入:文本向量化
-
相似度搜索:余弦相似度
-
Prompt工程:优化答案质量
学到的知识
-
RAG系统的完整实现流程
-
文档处理和分割策略
-
向量嵌入和相似度计算
-
信息检索和答案生成
-
系统集成和错误处理
运行效果示例
╔═══════════════════════════════════════╗
║ 📚 文档问答系统 v1.0 ║
╚═══════════════════════════════════════╝
✓ 系统初始化完成
正在加载示例文档...
✓ 已加载:sample1.txt
✓ 已加载:sample2.txt
✓ 已加载:sample3.txt
正在处理文档...
✓ sample1.txt: 3 个文档块
✓ sample2.txt: 2 个文档块
✓ sample3.txt: 3 个文档块
✓ 处理完成!共 3 个文档,8 个文档块
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
现在可以开始提问了!
输入 'help' 查看帮助,输入 'exit' 退出
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💬 您的问题: 什么是AI代理?
🔍 正在搜索相关信息...
📝 答案:
AI代理(AI Agent)是一个能够感知环境、做出决策并采取行动的智能系统。
它具有以下核心特征:
1. 自主性:能够独立做出决策
2. 反应性:能够感知环境变化并做出响应
3. 主动性:能够主动采取行动实现目标
4. 社交性:能够与其他代理或人类交互
常见应用包括智能客服助手、个人助理、自动化工作流等。
📚 参考来源:
1. sample1.txt (相关度: 0.92)
2. sample2.txt (相关度: 0.78)
💬 您的问题: 如何创建AI代理?
🔍 正在搜索相关信息...
📝 答案:
使用 Microsoft Agent Framework 创建代理的步骤:
1. 安装必要的包
2. 配置 Azure OpenAI 连接
3. 创建 ChatCompletionAgent 实例
4. 设置代理的指令(Instructions)
5. 调用代理处理用户输入
示例代码:
var agent = new ChatCompletionAgent
{
Name = "我的代理",
Instructions = "你是一个友好的助手",
Kernel = new Kernel()
};
📚 参考来源:
1. sample2.txt (相关度: 0.95)
扩展建议
1. 功能扩展
文档管理:
-
支持更多文档格式(PDF、Word、HTML)
-
实现文档更新和删除
-
添加文档分类和标签
检索优化:
-
实现混合检索(关键词 + 向量)
-
添加重排序(Reranking)
-
支持多语言检索
答案优化:
-
添加答案评分机制
-
实现流式输出
-
支持多轮对话
2. 性能优化
缓存策略:
public class CachedEmbeddingService
{
private readonly Dictionary<string, float[]> _cache = new();
public async Task<float[]> GenerateAsync(string text)
{
if (_cache.TryGetValue(text, out var cached))
{
return cached;
}
var embedding = await _embeddingService.GenerateAsync(text);
_cache[text] = embedding;
return embedding;
}
}
批量处理:
// 批量生成嵌入,提高效率
var embeddings = await embeddingService.GenerateBatchAsync(
chunks.Select(c => c.Content).ToList()
);
3. 持久化存储
使用真实向量数据库:
-
Qdrant
-
Milvus
-
Pinecone
-
Weaviate
示例(Qdrant):
// 连接到 Qdrant
var client = new QdrantClient("localhost", 6334);
// 创建集合
await client.CreateCollectionAsync("documents", 1536);
// 插入向量
await client.UpsertAsync("documents", new[]
{
new PointStruct
{
Id = chunk.Id,
Vector = embedding,
Payload = chunk.Metadata
}
});
// 搜索
var results = await client.SearchAsync("documents", queryVector, limit: 5);
4. Web界面
使用 Blazor 或 ASP.NET Core:
[ApiController]
[Route("api/[controller]")]
public class QAController : ControllerBase
{
private readonly RAGService _ragService;
[HttpPost("ask")]
public async Task<ActionResult<RAGResponse>> Ask([FromBody] QuestionRequest request)
{
var response = await _ragService.AnswerAsync(request.Question);
return Ok(response);
}
}
5. 评估和监控
答案质量评估:
public class AnswerEvaluator
{
public async Task<EvaluationResult> EvaluateAsync(
string question,
string answer,
List<string> sources)
{
// 评估答案的:
// 1. 准确性
// 2. 完整性
// 3. 相关性
// 4. 可读性
return new EvaluationResult
{
Accuracy = 0.9f,
Completeness = 0.85f,
Relevance = 0.92f,
Readability = 0.88f
};
}
}
性能监控:
public class PerformanceMonitor
{
public void LogQuery(string question, TimeSpan duration, int chunksRetrieved)
{
Console.WriteLine($@"
查询性能:
问题:{question}
耗时:{duration.TotalMilliseconds}ms
检索块数:{chunksRetrieved}
平均相关度:{/* 计算 */}
");
}
}
最佳实践
1. 文档准备
-
确保文档质量高、结构清晰
-
移除无关内容(页眉、页脚等)
-
保持文档格式一致
2. 分块策略
-
根据文档类型调整块大小
-
保持语义完整性
-
适当的重叠避免信息丢失
3. Prompt优化
-
明确指示基于文档回答
-
要求引用来源
-
处理信息不足的情况
4. 错误处理
-
优雅处理文件读取错误
-
处理API调用失败
-
提供友好的错误信息
5. 用户体验
-
显示处理进度
-
提供清晰的反馈
-
支持多种交互方式
项目价值
通过完成这个项目,你已经掌握了:
-
RAG技术:当前最重要的AI应用技术之一
-
向量检索:信息检索的核心技术
-
系统集成:如何整合多个组件
-
实践经验:从设计到实现的完整流程
这些技能可以应用到:
-
企业知识库问答
-
技术文档助手
-
客户支持系统
-
个人知识管理
-
研究辅助工具
继续学习
推荐资源:
-
LangChain 文档
-
LlamaIndex 文档
-
向量数据库文档
-
RAG 相关论文
恭喜你完成了这个项目!继续探索 AI 的无限可能!🚀
更多推荐



所有评论(0)