建议查看相关链接获取项目核心知识
GIT仓库 | 项目概述 | OpenHarness 源码 | OpenHarness 架构说明
版本:v0.1.0 | 阶段:P0 — Agent 核心可用版 | 日期:2026-04
运行截图演示
1. P0 核心目标
P0 阶段的核心命题是:验证 Resume Agent 的技术路线可行性。具体而言,需要证明以下三个关键假设:
假设一:精简裁剪可行
OpenHarness 是一个面向本地 CLI 的重量级 Agent 框架(43+ 工具、多 Provider、MCP stdio、Sandbox、Swarm),能否精准裁剪出适合云端简历场景的最小可用集?
P0 验证结论:可行。RuntimeBundle 成功将 OpenHarness 裁剪为仅保留 OpenAICompatibleClient + 3 个领域工具 + AUTO 权限模式的精简架构,同时保留了 Agent Loop 的完整能力。
假设二:领域提示词 + 记忆注入可产生高质量简历
通过"角色提示词 + 用户记忆 + 领域 Skill"三层组装的系统提示词,LLM 能否稳定输出结构化的 Markdown 简历?记忆系统能否实现"越用越好用"?
P0 验证结论:可行。三层组装机制使 LLM 能稳定输出包含个人简介、工作经历(STAR 法则)、技能标签的结构化简历;memory_write 工具使 LLM 可主动持久化用户偏好,跨会话生效。
假设三:SSE 流式对话可跑通端到端闭环
从 Web 页面输入 → FastAPI SSE 端点 → Agent Loop → DeepSeek API → 流式输出 → 页面渲染,这条链路能否完整跑通?
P0 验证结论:可行。极简 HTML 页面通过 EventSource 实现 SSE 流式对话,text_delta 逐字渲染,assistant_turn_complete 标记完成,端到端闭环已验证。
2. P0 实现逻辑
P0 的实现围绕一条核心链路展开:
用户输入 → SessionPool 获取/创建会话 → 提示词三层组装 → Agent Loop → SSE 流式输出
下面按数据流经的模块顺序,阐述每个环节的实现逻辑。
2.1 入口:FastAPI + SSE 端点
实现文件:backend/app.py、backend/routes/chat.py
实现逻辑:
- FastAPI 应用启动时,在
lifespan()中校验 API 配置并初始化SessionPool - 用户发送
POST /api/chat {prompt, session_id},chat 路由从 SessionPool 获取RuntimeBundle - 调用
bundle.engine.submit_message(prompt)获取AsyncIterator[StreamEvent] - 将每个
StreamEvent通过format_sse_data()序列化为data: {...}\n\n格式推送给客户端
P0 简化:开发模式下跳过认证,所有请求使用默认 user_id="dev_user"。
2.2 会话管理:SessionPool
实现文件:resume_agent/session_pool.py
实现逻辑:
get_or_create(user_id, session_id)
├── 命中缓存 → 更新 last_access → 返回已有 RuntimeBundle
└── 未命中 → build_resume_runtime() → 存入 _entries → 返回新 RuntimeBundle
- 会话 Key 设计:
{channel}:{user_id}:{session_id or 'default'},为后续多渠道接入预留 - 淘汰策略:5 分钟定时扫描,30 分钟空闲淘汰,容量满时强制 LRU 淘汰
- 并发安全:
asyncio.Lock保护_entries的读写
P0 简化:_save_snapshot() 为空实现,服务重启后对话历史丢失。
2.3 运行时构建:RuntimeBundle
实现文件:resume_agent/runtime.py
实现逻辑:
build_resume_runtime(user_id, session_id)
├── validate_api_config() → 缺 Key 则拒绝启动
├── build_resume_system_prompt(user_id) → 三层组装(见 2.4)
├── _get_shared_api_client() → 进程级单例,连接池复用
├── _get_shared_tool_registry() → 进程级单例,工具定义共享
├── _build_auto_permission_checker() → 固定 FULL_AUTO
├── _get_shared_hook_executor() → 进程级单例,P0 空实现
└── QueryEngine(...) → 每会话独立,对话历史隔离
共享 vs 隔离的设计逻辑:
| 对象 | 生命周期 | 理由 |
|---|---|---|
| OpenAICompatibleClient | 进程级共享 | 连接池复用,10 并发共享同一连接 |
| ToolRegistry | 进程级共享 | 工具定义不变,执行时从 metadata 获取 user_id |
| HookExecutor | 进程级共享 | P0 空实现,后续加载全局钩子 |
| QueryEngine | 每会话独立 | 对话历史必须隔离,避免跨会话污染 |
| System Prompt | 每会话独立 | 包含用户专属记忆,按 user_id 差异化注入 |
P0 裁剪对照:
| 环节 | OpenHarness 原版 | P0 精简版 |
|---|---|---|
| API Client | 多 Provider(OpenAI + Anthropic) | 仅 OpenAICompatibleClient → DeepSeek |
| MCP Manager | stdio + HTTP | 不创建 |
| Tool Registry | 43+ 内置工具 | 仅 domain 工具 |
| Permission | DEFAULT 需交互确认 | 固定 AUTO |
| Sandbox / Swarm | 可选创建 | 不创建 |
2.4 提示词组装:三层注入
实现文件:resume_agent/prompts/system_prompt.py
实现逻辑:
build_resume_system_prompt(user_id)
│
├── 第一层:RESUME_AGENT_SYSTEM_PROMPT
│ 角色定义 + 核心能力 + 输出格式约束 + 工作原则
│ → 赋予 LLM "简历优化顾问" 身份,约束输出为 Markdown 简历格式
│
├── 第二层:load_memory_documents(user_id)
│ 按 user_id 加载记忆目录下的 .md 文件
│ → 注入用户的简历原文、职业偏好、技能标签、优化历史
│ → 容量控制:简历原文 16KB,其余 4KB,超出截断
│
└── 第三层:load_resume_skill()
加载 resume_agent/skills/resume-skill.md
→ 注入简历优化领域知识:ATS 友好、STAR 法则、量化成果、关键词匹配
为什么是三层而非一次性写入:
- 第一层是稳定的角色约束,不随用户变化
- 第二层是用户专属上下文,按
user_id差异化注入 - 第三层是领域知识,可被
skill_loader工具动态重新加载
2.5 Agent Loop:对话引擎
实现文件:resume_agent/engine/query_engine.py、resume_agent/engine/query.py
实现逻辑:
QueryEngine.submit_message(prompt)
↓ 追加用户消息到 _messages
↓ 构建 QueryContext(包含所有依赖)
↓
run_query(context, messages) — Agent Loop
│
├── 1. auto_compact_if_needed()
│ 检查上下文 token 数,超出阈值则自动压缩历史消息
│
├── 2. OpenAICompatibleClient.stream_message()
│ → DeepSeek API 流式请求
│ → yield ApiTextDeltaEvent(逐字文本)
│ → yield ApiMessageCompleteEvent(完整消息 + usage)
│
├── 3. 解析响应
│ ├── stop_reason == "stop" → 输出完成
│ └── stop_reason == "tool_use" → 提取 ToolUseBlock
│
├── 4. execute_tool()
│ 从 ToolRegistry 查找工具 → 执行 → 返回 ToolResult
│ → memory_write: 持久化用户偏好/技能/优化历史
│ → skill_loader: 重新加载 resume-skill.md
│ → web_fetch: 抓取 JD 链接内容
│
├── 5. 追加工具结果到 messages,回到步骤 2
│ (最多循环 max_turns=8 轮)
│
└── 6. yield StreamEvent 流
→ TextDelta / ToolExecutionStarted / ToolExecutionCompleted / AssistantTurnComplete
2.6 API 对接:DeepSeek 客户端
实现文件:resume_agent/api/openai_client.py
实现逻辑:
- 消息转换:将内部
ConversationMessage转换为 OpenAI Chat Format(_convert_messages_to_openai),包含system/user/assistant/tool四种角色 - 工具格式转换:将
ToolRegistry中的工具定义转换为 OpenAI function-calling 格式(_convert_tools_to_openai) - 流式请求:
AsyncOpenAI.chat.completions.create(stream=True)逐 chunk 解析 - 重试机制:指数退避(1s → 2s → 4s,最大 30s),自动重试 429/500/502/503
- 错误分类:401/403 →
AuthenticationFailure,429 →RateLimitFailure,其他 →RequestFailure
多 Key 轮询池(resume_agent/api_key_pool.py):
class ApiKeyPool:
"""按 Key 维护令牌桶,轮询策略公平分配请求。"""
async def acquire(timeout=30.0) → str # 获取可用 Key
def report_429(key, suspend_seconds=10.0) # 报告 429,暂停该 Key
⚠️ P0 状态:
ApiKeyPool已实现但未集成到OpenAICompatibleClient,当前仅使用第一个 Key。
2.7 Agent 工具集
实现文件:resume_agent/tools/memory_write.py、skill_loader.py、web_fetch.py
三个工具构成 Agent 的"感知-行动"闭环:
| 工具 | 逻辑 | 关键约束 |
|---|---|---|
memory_write | 从 context.metadata 获取 user_id,调用 write_memory_file() 写入指定记忆文件 | 白名单控制(仅 职业偏好/技能标签/优化历史 可写),append/replace 模式,容量控制 |
skill_loader | 读取 resume_agent/skills/resume-skill.md,返回完整内容 | 只读,仅支持 resume-skill |
web_fetch | httpx 异步 GET 请求,提取 HTML 正文文本 | 仅 HTTP/HTTPS,10s 超时,5 分钟缓存,截断 4000 字符 |
"越用越好用"的实现路径:
第 1 次:用户输入信息 → Agent 生成简历 → 调用 memory_write 记录优化历史
第 2 次:系统提示词包含记忆 → Agent 自动遵循偏好 → 主动调用 memory_write 更新
第 3 次:记忆持续积累 → 生成质量持续提升 → 无需重复说明偏好
⚠️ P0 已知问题:三个工具已实现但未在
_get_shared_tool_registry()中注册,需在runtime.py中添加注册逻辑后激活。
2.8 SSE 事件序列化
实现文件:resume_agent/models/sse_events.py
实现逻辑:
StreamEvent (内部事件)
↓ sse_event_to_dict()
↓ 按事件类型提取字段
dict
↓ json.dumps(ensure_ascii=False)
↓ format_sse_data()
"data: {\"type\": \"text_delta\", \"text\": \"...\"}\n\n"
P0 定义了 9 种 SSE 事件类型:
| 事件类型 | 触发时机 | 客户端处理 |
|---|---|---|
text_delta | LLM 逐字输出 | 追加到助手消息气泡 |
tool_execution_started | 工具开始执行 | 显示工具调用提示 |
tool_execution_completed | 工具执行完毕 | 显示工具结果 |
status | 系统状态变更 | 更新状态栏 |
error | 发生错误 | 显示错误提示 |
assistant_turn_complete | 本轮对话完成 | 标记完成,启用输入框 |
ping | 心跳保活 | 忽略 |
resume_generated | 简历生成完成 | 显示下载提示(P1 启用) |
connection_timeout | 连接超时 | 提示重连 |
3. 配置体系
3.1 配置加载优先级
.env 文件 (python-dotenv 自动加载)
↓ 覆盖
环境变量 (DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL / DEEPSEEK_MODEL)
↓ 覆盖
全局配置文件 (~/.resume_agent/settings.json)
↓ 覆盖
代码内默认值
3.2 快速配置
cp .env.example .env
# 编辑 .env,填写 DEEPSEEK_API_KEY=sk-your-api-key-here
3.3 核心配置项
| 配置项 | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
| API Key | DEEPSEEK_API_KEY | — | 必填,支持逗号分隔多 Key |
| Base URL | DEEPSEEK_BASE_URL | https://api.deepseek.com | API 地址 |
| Model | DEEPSEEK_MODEL | deepseek-chat | 模型名称 |
| Max Tokens | — | 4096 | 单次最大输出 token 数 |
| Max Turns | — | 8 | Agent Loop 最大循环轮次 |
| Context Window | — | 64000 | 上下文窗口大小 |
| Max Sessions | — | 20 | SessionPool 最大会话数 |
| Idle Timeout | — | 1800 | 会话空闲超时(秒) |
4. 代码目录结构
ResumeHarness/
├── backend/ # FastAPI Web 服务层
│ ├── app.py # 应用入口 + 生命周期管理
│ └── routes/
│ └── chat.py # POST /api/chat SSE 端点
├── frontend/
│ ├── index.html # 极简验证页面
│ └── 测试数据.txt # 测试用例文档
├── resume_agent/ # 核心 Agent 逻辑包
│ ├── __init__.py
│ ├── runtime.py # RuntimeBundle 构建 + 进程级单例
│ ├── session_pool.py # 多租户会话池 (LRU 淘汰)
│ ├── api_key_pool.py # DeepSeek 多 Key 轮询池
│ ├── exceptions.py # 统一错误码与异常定义
│ ├── api/
│ │ ├── client.py # SupportsStreamingMessages 协议
│ │ ├── openai_client.py # OpenAI 兼容客户端
│ │ ├── errors.py # API 错误分类
│ │ └── usage.py # Token 用量追踪
│ ├── config/
│ │ └── settings.py # 全局配置加载
│ ├── engine/
│ │ ├── query_engine.py # 对话引擎(每会话独立)
│ │ ├── query.py # Agent Loop 核心循环
│ │ ├── messages.py # 消息模型
│ │ ├── stream_events.py # 内部流式事件
│ │ └── cost_tracker.py # 用量统计
│ ├── hooks/
│ │ ├── executor.py # 钩子执行器
│ │ └── loader.py # 钩子注册表
│ ├── memory/
│ │ ├── manager.py # 记忆加载/写入/容量控制
│ │ └── paths.py # 用户记忆目录路径管理
│ ├── models/
│ │ ├── sse_events.py # SSE 事件类型定义
│ │ └── api_schemas.py # 请求/响应 Pydantic 模型
│ ├── permissions/
│ │ ├── checker.py # 权限检查器
│ │ └── modes.py # 权限模式枚举
│ ├── prompts/
│ │ └── system_prompt.py # 系统提示词模板 + 三层组装逻辑
│ ├── services/
│ │ ├── session_storage.py # 会话快照持久化(已实现,未对接)
│ │ └── compact.py # 上下文压缩
│ ├── skills/
│ │ └── resume-skill.md # 简历优化领域知识
│ ├── templates/ # 简历 CSS 模板 (P1 阶段启用)
│ └── tools/
│ ├── base.py # BaseTool + ToolRegistry + ToolResult
│ ├── memory_write.py # 记忆写入工具
│ ├── skill_loader.py # 技能加载工具
│ └── web_fetch.py # 网页抓取工具
├── tests/ # 单元测试
├── .env.example # 环境变量配置模板
├── pyproject.toml # 项目配置与依赖
├── requirements.txt # Python 依赖列表
└── README.md # 项目说明文档
5. 快速启动
# 1. 创建并激活虚拟环境
python -m venv .venv
# Windows CMD:
.venv\Scripts\activate.bat
# macOS / Linux:
source .venv/bin/activate
# 2. 安装依赖
pip install -r requirements.txt
# 3. 配置 API Key
cp .env.example .env
# 编辑 .env,填写 DEEPSEEK_API_KEY=sk-your-api-key-here
# 4. 启动服务
python -m uvicorn backend.app:app --host 0.0.0.0 --port 8000 --reload
# 5. 验证
# 浏览器访问 http://localhost:8000 → 极简验证页面
# 浏览器访问 http://localhost:8000/docs → Swagger API 文档
6. P0 已知限制与后续规划
6.1 已知限制
| 限制 | 影响 | 修复阶段 | 修复方案 |
|---|---|---|---|
| 工具未注册到 ToolRegistry | 模型无法调用 memory_write/skill_loader/web_fetch | P0 补丁 | 在 runtime.py 的 _get_shared_tool_registry() 中注册三个工具 |
| ApiKeyPool 未集成 | 多 Key 轮询不生效,仅使用第一个 Key | P1 | 在 OpenAICompatibleClient 中集成 ApiKeyPool.acquire()/report_429() |
| 会话快照未持久化 | 服务重启后对话历史丢失 | P1 | 在 _save_snapshot() 中调用 session_storage.save_session_snapshot() |
| 无认证 | 任何人可直接访问 | P2 | 引入 JWT 认证中间件 |
| 无简历渲染下载 | 只能查看 Markdown 内容 | P1 | 实现 resume_renderer.py + weasyprint |
6.2 P1 迭代重点
| 任务 | 说明 |
|---|---|
| 简历渲染与下载 | Markdown → PDF (weasyprint),渲染队列,简历快照持久化 |
| 记忆管理 API | 记忆 CRUD + 简历上传 + 用户级 Settings |
| 工具注册激活 | 将三个领域工具注册到 ToolRegistry,激活 Agent 工具调用能力 |
| 工具与系统 API | 工具列表/会话列表/MCP 状态查询 |
| ApiKeyPool 集成 | 多 Key 轮询生效,429 自动切换 |
7. 测试验证
7.1 自动化测试
python -m pytest tests/ -v
覆盖范围:包导入、配置加载、API Key 合并、工具定义、提示词模板、消息模型等。
7.2 手动验证场景
详见 frontend/测试数据.txt,包含 8 个测试场景:
- 服务启动与健康检查
- API 文档访问
- SSE 流式对话(curl)
- 极简页面对话
- 简历生成对话
- 配置校验(无 API Key 时拒绝启动)
- 单测运行
- 工具调用验证
附录 A:关键依赖
| 依赖 | 版本 | 用途 |
|---|---|---|
| fastapi | >=0.110.0 | Web 框架 |
| uvicorn | >=0.29.0 | ASGI 服务器 |
| openai | >=1.23.0 | DeepSeek API SDK |
| pydantic | >=2.0 | 数据验证与模型 |
| httpx | >=0.27.0 | 异步 HTTP 客户端 |
| python-dotenv | >=1.0.0 | .env 文件加载 |
附录 B:错误码参考
| 错误码 | 含义 | HTTP 状态码 |
|---|---|---|
| 1001 | 用户未认证 | 401 |
| 1002 | Token 过期 | 401 |
| 2001 | DeepSeek API 调用失败 | 502 |
| 2002 | 速率限制 | 429 |
| 3001 | 会话不存在 | 404 |
| 3002 | 会话已过期 | 410 |
| 4001 | MCP 服务不可用 | 503 |
| 4002 | 简历渲染失败 | 500 |
| 4003 | 简历不存在 | 404 |
| 5001 | 记忆文档不存在 | 404 |
| 6001 | 网页抓取失败 | 502 |