03 — 智能体框架,深度解析
2026/7/30 20:36:10 · 更新于 2026/7/30 22:48:18
AI 翻译于 2026/7/30 22:19:41 · 使用 Qwen3.6 35B (fast, default)
对极简手写代理架构的深入技术解析——零依赖框架:逐一讲解请求认证、系统提示词构建、轮次式工具编排、统一工具注册(手动代码 + MCP)、用户记忆管理、ChromaDB 检索及来源权威性排序,以及流式与非流式实现的权衡。
本文档详细介绍了 apps/agent-server(一个基于 FastAPI、httpx、ChromaDB 和 MCP SDK 构建的轻量级自研智能体框架)的架构、请求生命周期及核心子系统。未使用任何外部框架;每一层——从身份验证路由到内存管理——均为显式实现,以确保可预测的调试体验和易于扩展的操作。
请求生命周期与执行流程
请求通过 POST /v1/chat/completions 进入,附带 Bearer API 密钥,格式与 OpenAI 原生 API 规范保持一致。处理流程遵循严格顺序:
- 身份验证在解析请求体之前运行。
auth.authenticate()会验证携带令牌(bearer token)的哈希值是否与keystore.py中的已知密钥匹配(该文件仅存储 SHA-256 哈希值;原始密钥仅在创建时显示一次且不可恢复),确认密钥已启用且未过期,验证所需的作用域(chat、tools或admin),检查每分钟速率限制,并验证每日令牌配额。 - 系统提示词组合在每次请求时从头重新生成。
compose_system_prompt()会拼接以下内容:- 来自
AGENT.md的基础身份、工具使用策略及安全护栏(每次直接从磁盘读取,支持即时更新行为逻辑而无需重启)。 - 至多一个匹配的技能块(通过
skills/loader.py按关键词驱动)。 - 针对特定用户的通俗语言记忆简报(由
_compose_memory_section()生成,从memory_store.py拉取数据)。
- 来自
- 自引用路由在授予工具访问权限之前拦截提示词。
is_self_referential()用于判断查询目标是否为智能体自身而非领域内容。如果为真,则仅对该请求移除工具访问权限(详见下文)。 - 编排逻辑在
graph.py中执行,这是一个运行“批次(waves)”任务的轻量级定制引擎。给定步骤中的所有独立操作均通过asyncio.gather并发执行。结果会合并到共享状态对象中,下一批任务根据各节点声明的后续操作进行组装。模型发出的工具调用将在下一批生成独立的节点,从而实现真正的并行执行而非顺序轮询。 - 工具钩子(hooks)强制生命周期边界。 前置工具钩子可完全拦截执行(例如,在知识库文件夹内强制执行只写边界)。后置工具钩子触发副作用(例如,在写入成功后启动后台重新索引)。两者均具备容错能力:钩子报错时仅记录日志并跳过,而不会导致请求崩溃。
- 模型转发被抽象为单一函数。
_prepare_upstream_body()负责处理 Ollama API 调用的所有格式转换。这是修改模型调用逻辑的确切位置(参见07_Switching_LLM_Models_Guide.md)。 - 内存更新异步运行。 图处理收敛后,
memory_pipeline.analyze_and_record()会作为后台任务触发。它绝不会阻塞或延迟调用者接收到的响应。流式请求遵循一条并行但重复的路径。由于流式传输需要持续发射令牌(token),它绕过了graph.py的基于波次(wave-based)的最终状态模型。相反,run_streaming()手动实现了相同的“规划器-然后-工具”循环,复用完全相同的钩子(hooks)和工具注册表,以保持行为的一致性。注意: 这种重复意味着工具轮次预算和钩子排序被实现了两次,需要手动同步——这是一个在后续部分解决的文件维护开销。
工具注册表与外部集成
tools/registry.py 维护着两个并行的数据结构:一个是工具模式列表(暴露给模型),另一个是将名称映射到可执行 Python 函数的字典。添加原生工具(如 search_knowledge_base 或 get_study_plan)需要实现该函数并在两个结构中将其注册。
外部工具通过**模型上下文协议(MCP)**进行集成。mcp_client.py:
- 解析
mcp_servers.json。 - 将配置的服务器作为本地子进程启动。
- 查询每个服务器以获取可用工具,并以
mcp__<server>__<tool>前缀将它们注册到统一的注册表中。 这种抽象确保了agent_loop.py无法区分手写工具和外部工具。内置了故障隔离机制:如果 MCP 服务器无法连接,它将注册零个并发出警告,而不会破坏核心功能。目前,两个已配置的服务器(fetch、secedgar)由于 SDK 不兼容仍处于禁用状态——这生动地展示了此故障边界按预期工作。 活动集成: filesystem(限定于知识库目录范围)sequentialthinking(结构化规划/推理)pdfreadertavily(网络搜索) 当前约束: 仅支持 stdio 传输协议。尚无法通过此客户端连接需要 HTTP 或 SSE 的 MCP 服务器。
内存管理与状态规则
memory_store.py 在分区的 SQLite 数据库中管理每个用户的事实(年级水平、学科重点、已记录的知识差距)。用户通过调用者提供的标识符来识别,如果没有提供,则默认为 API 密钥的 ID。
一个关键的架构规则约束着所有写入操作:
只有显式的用户输入才能更新内存。 检索到的文档和模型生成的文本不能写入或改变存储的事实。这防止了幻觉内容或检索到的断言悄悄变成被信任的关于用户的状态。 在提升为稳定状态(足够可信以进行自信断言)之前,事实需要经过多天的佐证过程。年级水平 specifically 需要用户显式的陈述,且永远不能从上下文中推断得出。
检索与来源权威tools/knowledge_base.py 中的文档检索遵循严格的两个阶段流水线:
- **嵌入与搜索:**查询使用
nomic-embed-text(即用于索引的同一模型)进行嵌入,并通过余弦相似度与 ChromaDB 集合进行匹配。选择这一指标(而非默认的距离指标)是刻意为之,因为它测量的是矢量方向而非原始幅度,这与密集文本嵌入更为契合。 - **来源权威性重排序:**一个轻量级的可配置层会将结果引导至更高信任度的来源,而不会覆盖真实的关联相关性。添加此功能是为了解决实际运行中的一类故障模式:在该模式下,次要摘要文档的排名总是高于它们本应支撑的主要法规。
自引用路由与护栏
路由门控在昂贵的推理过程启动之前充当低成本过滤器:
- **正则层级:**经过筛选的列表会检查确切的措辞(例如“你是谁”、“你遵循哪些护栏规则”)。它严格将匹配范围限制为简短问题,并排除包含领域词汇的查询(防止因无意间使用共有词汇而导致预算调整类等误报)。
- **嵌入层级:**仅在正则匹配未命中时触发。提示词将被嵌入,与标准的“关于代理”示例通过余弦相似度进行比对,并根据 0.80 的阈值进行评估。该值是根据日志记录的相似度分数经验校准得出的,旨在捕获真实自引用提示词的同时,最大化防范误报的安全裕度。
每一次路由决策(包括接近命中的情况)都会记录到
logs/agents/triage.jsonl中,这使得未来的阈值调整可以基于生产数据进行,而无需依赖静态配置。代码强制执行的护栏包括:
- 故障安全(fail-closed)的写入路径钩子,默认阻止未识别或格式错误的写入操作。
- 所有工具调用的强制日志记录。
- 严格的凭证隔离:代理循环绝不会接收外部凭证(例如 Tailscale 管理员密钥、备份驱动器访问权限),仅在沙盒文件夹内运行。**已记录的缺陷:**写入执行前不存在人工审批环节。文件夹沙盒依然是防止错误或受操纵的输出被立即执行的唯一屏障。
架构权衡
该 Harness(运行环境)旨在实现可预测性和可扩展性,但伴随着有意为之的权衡:| 优势 | 劣势 | |----------|----------| | 统一的工具注册中心: 手写工具和 MCP 工具共享相同的接口,使得新功能可以通过单一注册行插拔。 | 重复的执行路径: 流式和非流式的控制流分别实现,需要手动同步预算、钩子和速率限制。 | | 通用的钩子系统: 预/后事件将副作用与工具逻辑解耦,在扩展时保持核心循环不变。 | 进程内状态管理: 缓存和速率限制器仅存在于进程内存中,一旦引入水平扩展或多个实例,就需要重构。 |
这两种权衡均在 05_Gap_Analysis.md 和 08_Extending_The_Harness_Agent.md 中完整记录并有解决路径。
关键要点
- 请求处理严格顺序且透明: 认证 → 提示词构建 → 路由检查 → 波次编排 → 模型转发 → 后台记忆更新。
- 工具与框架无关: 统一的注册中心将手写函数和 MCP 集成的工具体系化抽象为相同的可调用模式,并对服务端故障进行优雅隔离。
- 记忆严格按用户驱动: ChromaDB 的检索和模型生成不会改变存储状态;事实需要多天的证实才能变为稳定态。
- 护栏是代码强制执行的,而非口号式的: 写入失败时关闭,凭证永远不离开沙箱,且工具调用完全可审计。扩展和流式路径重复仍然是主要需要架构演进的领域。
学习地图
学习路径:构建自定义代理(Agent)框架
第一阶段 —— 基础
- 理解代理架构基础:“路由-提示词-模型-循环”模式
- 使用 Pydantic 请求/响应模型搭建 FastAPI 服务器
- 配置上游大语言模型提供商(如 Ollama、OpenAI 等)
第二阶段 —— 请求管线
- 实现基于 SHA-256 密钥哈希的 Bearer token 认证,并加入作用域与速率限制检查
- 构建动态系统提示词合成器:基础身份标识 + 匹配技能模块 + 用户画像摘要
- 添加自指问题过滤机制(正则表达式 + 嵌入层)
第三阶段 —— 工具系统
- 设计双源工具注册表:手写函数 + MCP 服务器
- 注册工具 Schema(供模型端使用)和处理器函数(用于执行端)
- 实现用于验证与副作用处理的前置/后置钩子
第四阶段 —— 执行引擎
- 使用 asyncio.gather 构建基于波次的编排器,支持并行工具调用
- 将结果状态路由回下一波次的节点图
- 单独处理流式路径,配置独立循环但共享底层工具与钩子逻辑
第五阶段 —— 内存与检索
- 搭建用户级 SQLite 记忆存储(仅持久化用户输入的事实,绝不存储模型生成的内容)
- 对 ChromaDB 进行嵌入向量化与检索,结合余弦相似度与来源权威性重排序
- 将短期事实提升为
动手实践——分步指南
- 搭建 FastAPI 项目,创建 /v1/chat/completions 端点,使其镜像 OpenAI 的 API 结构。
- 使用 keystore.py 构建 auth.authenticate() 及 SHA-256 哈希——存储密钥哈希,并允许密钥仅在创建时显示一次。
- 编写 compose_system_prompt(),从磁盘读取 AGENT.md,通过 skills/loader.py 的关键词重叠匹配加载一个技能,并将 memory_store.py 的记忆简报追加到其后。
- 在 tools/registry.py 中创建统一的工具注册表:面向模型的 schemas 列表和面向执行的 functions 字典。手动注册至少一个工具(如 search_knowledge_base)。
- 构建 mcp_client.py 以读取 mcp_servers.json,将每个服务器作为子进程启动,查询可用工具,并将它们注册到同一注册表中,使用前缀
mcp__<server>__<tool>。 - 实现 graph.py 的波次(wave)编排:按每波收集并发节点,将结果合并至共享状态,并调度后续节点,直到模型给出最终答案。
- 在 tools/registry.py 中添加前置和后置工具钩子链,用于写入路径验证(失败即关闭)及后台重新索引等副作用。
- 创建 memory_store.py,按调用者 ID 或密钥对用户维度的知识进行 SQLite 分区——仅从用户输入的文本中更新信息,并需多天的交叉确认后才提升为稳定状态。
- 在 tools/knowledge_base.py 中集成 ChromaDB 检索——使用与索引相同的嵌入器及余弦相似度进行查询匹配,并添加基于源权威度的重新排序加分。
- 实现 run_streaming(),手动构建用于 token 流式传输的规划-工具循环——使其与不使用流式传输的 graph.py 保持一致,共享同一注册表、钩子链和工具预算逻辑。
- 添加自指路由门:正则表达式的精确措辞层 + 基于嵌入的层(与标准"关于我"问题做余弦匹配),并附带可随时间重新调优的已记录阈值。
三大推荐资源
- 1FastAPI Documentation
Official FastAPI docs—essential for building the OpenAI-compatible routing endpoint and request validation.
https://fastapi.tiangolo.com/
- 2Model Context Protocol (MCP) — Official Specification & SDK
The spec and Python SDK for MCP—the protocol used to discover, register, and call external tools from the agent harness.
https://modelcontextprotocol.io/
- 3ChromaDB Documentation
Official ChromaDB docs—covers embedding models, cosine similarity, collections, and retrieval queries used in this architecture.
https://docs.trychroma.com/
链接由 AI 推荐——使用前建议快速核实。