00项目架构与选型
面试中很容易被问到为什么选择了某个技术而没选择另外一个技术,所以在项目进行之前要有架构设计与技术选型说明书,方便在面试中有条理的回答。
1.项目总体架构图

2. 核心模块技术选型矩阵
模块一:大模型底层调用(LLM Client)
| 候选技术 | 方案A:官方SDK(openai) | 方案B:requests裸调(我的选择) |
|---|---|---|
| 实现方式 | client.chat.completions.create() |
手写requests.post + 手写SSE逐行解析 |
| 优点 | 代码量极少,开箱即用 | 完全掌控HTTP细节,彻底看懂流式协议 |
| 缺点 | 屏蔽了重试、超时、流式解析的具体实现 | 代码量大,需手动处理Chunk边界 |
-
为什么不用官方SDK?
-
答:
因为面试和实际生产不同。在生产中我会用SDK,但在这个项目中,我的核心目标是搞懂底层原理。使用
requests裸调让我必须手动处理iter_lines解析data: [DONE],手动捕获429状态码做指数退避。这种'手撕协议'的过程让我深刻理解了SSE(Server-Sent Events)不是魔法,就是标准的HTTP长连接。如果将来SDK出Bug,我能直接看懂HTTP抓包去排查。
模块二:Agent架构框架(Agent Loop)
| 候选技术 | 方案A:LangGraph/LangChain | 方案B:纯Python While循环(我的选择) |
|---|---|---|
| 实现方式 | StateGraph + add_node + compile |
while max_iterations: + if tool_calls: |
| 优点 | 支持复杂分支、检查点、持久化 | 极轻量,每一行报错都能立刻定位 |
| 缺点 | 源码黑盒,出问题难排查,依赖重 | 需手动实现循环终止条件和状态合并 |
-
既然LangGraph是主流,你为什么非手搓?
-
答:
我的选型原则是奥卡姆剃刀。本项目Agent的拓扑结构是'规划-执行-观察'线性循环,不涉及复杂的条件分支或并行子图。用LangGraph属于'杀鸡用牛刀',而且它内部复杂的
Pregel执行器会掩盖状态流转的本质。我手写
while循环后,发现了一个关键认知:Agent的状态管理其实就是维护messages这个List。LangGraph的add_messagesReducer,底层无非就是list.extend()。这种认知让我在调试多轮对话时能精准预测内存中的State变化,而不是在黑盒外猜。
模块三:向量数据库(RAG Storage)
| 候选技术 | 方案A:Chroma / Pinecone | 方案B:NumPy矩阵 + JSON文件(我的选择) |
|---|---|---|
| 实现方式 | collection.query() |
np.dot(embeddings, query_vec) / (norm * norm) |
| 优点 | 支持海量数据、HNSW索引、分布式 | 纯数学运算,零网络开销,完全本地化 |
| 缺点 | 引入外部依赖,需维护服务进程 | 暴力搜索,数据量超10万条时性能下降 |
-
Chroma这么成熟,为什么不用?
-
答:
这就是典型的场景驱动选型。我的知识库预计只有1000-2000个文本块(个人笔记规模)。在这种规模下,暴力计算余弦相似度的耗时(毫秒级)和HNSW索引的耗时差距微乎其微。
但我选择NumPy手算有一个无法替代的面试价值:我手写过
cosine_similarity = A·B / (||A|| ||B||),所以我能清楚地告诉面试官——RAG检索的本质是线性代数,而不是神秘的'魔法API'。如果将来我转去做搜推引擎,这个数学基础是通用的。
模块四:MCP协议实现(Model Context Protocol)
| 候选技术 | 方案A:官方MCP Python SDK | 方案B:手写JSON-RPC 2.0 + stdio(我的选择) |
|---|---|---|
| 实现方式 | from mcp.server import Server |
sys.stdin.read() + json.dumps + sys.stdout.write |
| 优点 | 开箱即用,自动处理协议版本 | 完全搞懂JSON-RPC 2.0的消息格式 |
| 缺点 | 抽象层次高,难以理解底层通信 | 需手动处理消息边界(换行符分隔) |
-
MCP是新技术,官方有SDK为什么不用?
-
答:
我的目标不是'接入MCP',而是'成为MCP的贡献者'。我必须搞懂MCP的本质:它就是定义在
stdio或WebSocket上的JSON-RPC 2.0协议。手写实现让我看清了三个核心Method:
initialize(握手)、tools/list(暴露工具)、tools/call(执行调用)。我把Tools注册进MCP Server后,用Claude Desktop测试,看着JSON报文在终端打印出来,那一刻我彻底明白了进程间通信(IPC)和协议标准化的意义。如果只用SDK,我永远不会有这种'降维打击'的快感。
模块五:Web服务框架(API Server)
| 候选技术 | 方案A:FastAPI(异步) | 方案B:Flask + Gunicorn(我的选择) |
|---|---|---|
| 实现方式 | async def chat(): + await |
def chat(): + yield (SSE流式) |
| 优点 | 高性能并发,自动OpenAPI文档 | 同步阻塞逻辑简单,完美适配requests库 |
| 缺点 | 异步调试复杂,对新手不友好 | 高并发下(QPS>100)性能不如FastAPI |
-
现在新项目都用FastAPI,为什么选Flask?
-
答:
选型要匹配调用链路。我的Agent循环是同步阻塞的(调用OpenAI API等待返回),即便用了FastAPI的
async/await,底层的httpx异步客户端如果没配好,一样会阻塞事件循环。反而Flask搭配gunicorn的sync模式,利用多进程(-w 4)处理并发,架构更清晰,也更容易排查死锁。而且我的流式输出(SSE)用Flask的
Response(generate())生成器实现得极其优雅。如果将来真要扛高并发,我会水平扩展(多部署几个Flask实例+nginx负载均衡),而不是在单机异步上死磕,这在生产中更实用。
模块六:Skills vs Tools 的架构分层
你的Tools和Skills有什么区别?
答:
这是我在架构上做的最重要的分层解耦:
Tools(原子工具):只做一件事,不包含业务判断。比如
read_file(path)只返回字符串,write_file(path, content)只写磁盘。它们不具备智能,只提供能力。Skills(技能编排):是硬编码的工作流。比如我的
weekly_report_skill,它做了三件事:①调用read_file读取本周日志;②调用LLM接口(不带tools)进行文本提炼;③调用write_file保存Markdown。为什么不用Agent代替Skills? 因为Agent规划(Planning)太随机,3次调用可能有2次格式不对。对于标准化高频操作(如写日报),我用Skills固定死流程,保证100%成功率。只有面对陌生用户问题时,我才启动Agent进行动态规划。这是混合式架构,兼顾了稳定性和灵活性。
3. 数据流设计
也就是一条请求进来之后的全流程:
-
用户输入 → API (
Flask) 接收。 -
Agent规划器(
while第一轮,不带Tools)输出plan: "需要先查天气,再算温差"。 -
Agent执行器(
while第二轮,带Tools)调用get_weather→ 结果存进messages。 -
Agent执行器(
while第三轮)调用calculator→ 结果存进messages。 -
Agent反思器(检查
messages里的工具结果是否为空,若空则重试)。 -
最终输出:返回自然语言给用户。
-
旁路RAG:如果用户输入含"查一下我的笔记",Agent会先调
embedder,在NumPy矩阵里算余弦相似度,召回的文本块作为system前缀塞进messages。
4. 技术栈总览
| 层级 | 我的技术选型 | 面试防怼话术(为什么不用别的) |
|---|---|---|
| 网络协议 | 裸HTTP + 手写SSE | SDK屏蔽了流式解析,遇到[DONE]错位我无法排查 |
| Agent | while循环 + messages维护 |
LangGraph太重,线性循环无需有向无环图(DAG) |
| 向量存储 | NumPy + JSON | 数据量<1万条,暴力搜索精度最高,无网络开销 |
| Embedding | sentence-transformers | 开源离线,避免调用API被限流,且可控 |
| MCP | 手写JSON-RPC | 官方SDK依赖注入复杂,无法debug协议层 |
| Web框架 | Flask + Gunicorn | Agent是同步阻塞型,用异步反而增加心智负担 |
| 部署 | 轻量云 + systemd | 不用容器化,减少依赖冲突,让进程管理可视化 |
5. WBS(工作分解结构)
-
Week 1:实现
core/llm_client.py(非流式+流式),手写测试脚本直接跑通API。 -
Week 2:实现
rag/全部模块,手写余弦相似度,测试检索Recall。 -
Week 3:实现
tools/全部原子工具,每个工具写独立单元测试(用assert)。 -
Week 4:实现
core/agent_loop.py,挂载Tools,实现多轮调用。 -
Week 5:实现
skills/,硬编码一个周报技能,验证稳定性。 -
Week 6:实现
mcp/,启动本地Server接入Claude Desktop。 -
Week 7:写
api/routes.py,封装Flask接口。 -
Week 8:跑通
eval/评测,生成报告,部署上线。
更多推荐


所有评论(0)