OfferMate-RAG 是一个面向岗位 JD、简历与技术文档场景的智能求职助手项目,以 RAG(Retrieval-Augmented Generation) 为核心,以 Agent Workflow + Tool Calling 为增强,并引入 Harness Engineering 思路,通过模块边界、Schema 约束、Prompt 模板管理、配置中心、Tool Contract、测试与 CI 质量门禁,将 AI 能力收敛为可控、可复现、可交付的工程流程。
本项目面向大模型应用与 AI 工程场景,重点关注:
- 文档问答
- 岗位 JD 解析
- 简历解析
- 技能匹配
- 面试题生成
- Dense / BM25 / Hybrid Retrieval
- Agent Workflow
- Tool Contract
- Harness Engineering
- AI 系统可维护性与可控性
本项目旨在构建一个面向求职场景的检索增强智能助手,支持以下能力:
- 基于岗位 JD、简历与技术文档进行问答
- 输出带引用的可信回答
- 支持 Qwen Embedding 向量检索、BM25 关键词检索与 Hybrid Retrieval
- 对用户请求进行任务路由,并调用对应工具模块
- 解析岗位要求与简历内容,完成技能匹配分析
- 根据岗位与简历生成针对性面试问题
- 通过 Harness Engineering 降低 AI 系统输出的不确定性,提升系统可控性与可维护性
- 支持 PDF / TXT / Markdown 文档加载
- 支持文本 chunk 切分
- 支持 Qwen
text-embedding-v4向量检索 - 支持 BM25 关键词检索
- 支持 Hybrid Retrieval 混合检索
- 支持 Qwen
qwen-plus回答生成 - 支持 citation-aware answer
- 支持基础拒答机制
- 返回结构化
RAGResponse
当前检索模块支持三种模式:
| 模式 | 是否调用 Qwen | 说明 |
|---|---|---|
dense |
是 | 使用 Qwen text-embedding-v4 进行向量检索 |
bm25 |
否 | 使用 BM25 进行关键词检索 |
hybrid |
是 | 融合 Dense Retrieval 与 BM25 Retrieval |
可通过 config/retrieval.yaml 配置切换:
retrieval_mode: bm25可选值:
dense / bm25 / hybrid
- 支持 config-driven priority router
- 支持根据用户请求路由到不同任务路径
- 通过
Tool Registry统一管理可调用工具 - 通过
WorkflowResult统一约束 workflow 输出结构 - 当前支持任务类型:
- RAG 问答
- JD Parser
- Resume Parser
- Skill Matcher
- Interview Question Generator
当前已实现规则版工具模块:
JD Parser:抽取岗位技能、学历要求、实习时长、职责描述Resume Parser:抽取简历技能、教育背景、项目、奖项Skill Matcher:对比 JD 与简历,输出匹配技能、缺失技能、建议补充项和匹配分数Interview Question Generator:基于岗位要求、简历内容和技能差距生成分类型面试问题
当前 tools 采用 schema-based output:
JD Parser返回JDInfoResume Parser返回ResumeInfoSkill Matcher返回MatchResultInterview Question Generator返回InterviewQuestionSetAgent Workflow返回WorkflowResult
这使得工具输出不再是随意的 dict,而是具备固定字段、固定类型和可测试性的结构化结果。
本项目不仅关注 AI 功能本身,也关注 AI 系统的稳定交付。当前通过以下方式体现 Harness Engineering:
- 模块化架构:
rag / agent / tools / schemas / prompts / config / harness / tests - Schema 约束:使用 Pydantic 固定核心输入输出结构
- Prompt 外置:将 prompt 模板从代码中分离
- 配置中心:统一管理模型、检索、回答、路由、工具契约等配置
- Tool Contract:通过
config/tool.yaml声明工具输入、运行模式与输出结构 - 测试体系:使用 PyTest 对核心模块进行基础测试
- CI 质量门禁:使用 GitHub Actions 执行基础测试流程
flowchart TD
A[用户输入] --> B{任务路由 Router}
B -->|文档问答| C[RAG Pipeline]
B -->|岗位解析| D[JD Parser]
B -->|简历解析| E[Resume Parser]
B -->|技能匹配| F[Skill Matcher]
B -->|面试题生成| G[Interview Question Generator]
C --> C1[文档加载 Loader]
C1 --> C2[文本切分 Chunker]
C2 --> C3{Retrieval Mode}
C3 -->|dense| C4[Qwen text-embedding-v4 Dense Retrieval]
C3 -->|bm25| C5[BM25 Keyword Retrieval]
C3 -->|hybrid| C6[Dense + BM25 Hybrid Retrieval]
C4 --> C7[Top-K Contexts]
C5 --> C7
C6 --> C7
C7 --> C8[Qwen qwen-plus 回答生成]
C8 --> C9[RAGResponse: answer + citations + grounded]
D --> D1[JDInfo Schema]
E --> E1[ResumeInfo Schema]
F --> F1[MatchResult Schema]
G --> G1[InterviewQuestionSet Schema]
D1 --> W[WorkflowResult]
E1 --> W
F1 --> W
G1 --> W
C9 --> H[Streamlit 前端展示]
W --> H
H --> I[用户查看结果]
subgraph Harness Engineering
J[schemas: 输出结构约束]
K[prompts: Prompt 模板管理]
L[config: 参数配置中心]
TC[tool.yaml: Tool Contract]
M[tests: 单元测试]
N[CI: GitHub Actions]
end
C --> J
D --> J
E --> J
F --> J
G --> J
B --> L
C --> K
C --> L
D --> TC
E --> TC
F --> TC
G --> TC
M --> N
flowchart TD
A[原始文档输入<br/>PDF / TXT / Markdown] --> B[Loader 文档加载]
B --> B1[使用方法:<br/>PyMuPDF 解析 PDF<br/>read_text 读取 TXT / MD]
B1 --> C[统一文档结构<br/>text + metadata]
C --> D[Chunker 文本切分]
D --> D1[使用方法:<br/>固定窗口切分<br/>chunk_size = 500<br/>chunk_overlap = 100]
D1 --> E[DocumentChunk]
E --> E1[Chunk Metadata:<br/>chunk_id<br/>source<br/>file_name<br/>file_type<br/>page]
E1 --> F{Retrieval Mode}
F -->|dense| G[Dense Retrieval]
G --> G1[方法:<br/>Qwen text-embedding-v4<br/>Embedding 维度 1024<br/>向量相似度排序]
F -->|bm25| H[BM25 Retrieval]
H --> H1[方法:<br/>rank-bm25<br/>简单中英文 tokenizer<br/>关键词相关性排序]
F -->|hybrid| I[Hybrid Retrieval]
I --> I1[方法:<br/>Dense Score + BM25 Score<br/>min-max normalization<br/>hybrid_alpha 加权融合]
J[用户 Query] --> K[Query Processing]
K --> K1[Dense 模式:<br/>Qwen Embedding 编码 query]
K --> K2[BM25 模式:<br/>simple_tokenize query]
G1 --> L[Top-K Chunks]
H1 --> L
I1 --> L
L --> M[Context Assembly]
M --> M1[方法:<br/>拼接 Top-K chunks<br/>保留 citation metadata]
M1 --> N{是否满足回答阈值?}
N -->|top score 低于阈值| O[拒答]
O --> O1[返回:<br/>grounded = false<br/>citations = empty]
N -->|top score 达到阈值| P[Generator 回答生成]
P --> P1[方法:<br/>Qwen qwen-plus<br/>temperature = 0.2<br/>grounded prompt]
P1 --> Q[Citation Builder]
Q --> Q1[方法:<br/>根据 retrieval metadata 构建 citations]
Q1 --> R[RAGResponse]
R --> R1[输出结构:<br/>answer<br/>citations<br/>grounded]
| 模块 | 文件 | 当前方法 | 说明 |
|---|---|---|---|
| 文档加载 | rag/loader.py |
PyMuPDF + 本地文本读取 | PDF 使用 PyMuPDF,TXT/MD 使用 read_text |
| 文本切分 | rag/chunker.py |
固定窗口切分 | 使用 chunk_size 和 chunk_overlap 控制切分粒度 |
| Chunk Schema | schemas/document.py |
Pydantic Schema | 统一 chunk 输出结构,保留 source、file_name、page 等元数据 |
| Dense Retrieval | rag/retriever.py |
Qwen text-embedding-v4 |
使用 Qwen embedding 模型对 chunk 和 query 编码 |
| BM25 Retrieval | rag/retriever.py |
rank-bm25 |
使用轻量 tokenizer + BM25 进行关键词检索 |
| Hybrid Retrieval | rag/retriever.py |
Dense + BM25 加权融合 | 对 Dense 与 BM25 分数归一化后加权融合 |
| 生成 | rag/generator.py |
Qwen qwen-plus |
基于检索上下文生成回答 |
| 引用构建 | rag/pipeline.py |
metadata-based citation | 根据检索结果的 source、file_name、page、chunk_id 构建引用 |
| 拒答机制 | rag/pipeline.py + config/answer.yaml |
score threshold | 当 top score 低于阈值时返回拒答 |
| 输出结构 | schemas/common.py |
RAGResponse |
返回 answer、citations、grounded |
| 检索结果结构 | schemas/retrieval.py |
RetrievalResult |
记录 score、source、metadata 与 retrieval_type |
- 项目初始化
- 基础目录结构搭建
- FastAPI 最小后端启动
- Streamlit 最小前端启动
- Agent / Tools 模块骨架
- Schema 约束层初版
- Prompt 模板目录初版
- Config 配置目录初版
- 文档加载模块
loader.py - 文本切分模块
chunker.py -
load -> chunk最小 pipeline - Qwen Embedding 接入(
text-embedding-v4) - Qwen Generation 接入(
qwen-plus) - Dense Retrieval
- BM25 Retrieval
- Hybrid Retrieval
-
retrieval_mode支持dense / bm25 / hybrid配置切换 -
RetrievalResult增加retrieval_type字段 - 回答结构化输出(
RAGResponse/Citation) - 基础拒答机制(score threshold)
- 引用构建逻辑
- 最小
/chatAPI - Streamlit 问答演示页
- Config-driven priority router
- JD Parser 规则版
- Resume Parser 规则版
- Skill Matcher 规则版
- Interview Question Generator 增强规则版
- Agent Tool Registry
- Agent Workflow 初版
-
WorkflowResult统一 workflow 输出结构 -
InterviewQuestionSet统一面试题输出结构 - Tool Contract 配置文件
config/tool.yaml - Harness Tool Contract Check
- Harness Workflow Check
- Streamlit 支持 RAG 问答、JD/简历匹配、面试题生成、Router 调试
- 基础 Harness Checks
- GitHub Actions CI 初版
- 单元测试覆盖核心本地逻辑
- Reranker
- Benchmark / Regression Harness
- Retrieval Evaluation
- Recall@K / MRR 指标
- LLM Router Fallback
- Qwen-based Tool Generation
- 更完整的端到端 Agent Workflow
offermate-rag/
├── app/ # 前端展示层(Streamlit)
│ └── main.py
├── backend/ # API 层(FastAPI)
│ └── main.py
├── rag/ # RAG 主链路
│ ├── loader.py # 文档加载
│ ├── chunker.py # 文本切分
│ ├── retriever.py # Dense / BM25 / Hybrid 检索
│ ├── generator.py # Qwen generation 回答生成
│ └── pipeline.py # retrieval -> answer 主流程
├── agent/ # Agent 路由与流程编排
│ ├── router.py # priority-based router
│ ├── registry.py # tool registry
│ └── workflow.py # agent workflow
├── tools/ # 可被 agent 调用的工具模块
│ ├── jd_parser.py
│ ├── resume_parser.py
│ ├── skill_matcher.py
│ └── interview_generator.py
├── schemas/ # 统一输入输出约束
│ ├── common.py # Citation / RAGResponse
│ ├── jd.py # JDInfo
│ ├── resume.py # ResumeInfo / ProjectInfo
│ ├── match.py # MatchResult
│ ├── document.py # DocumentChunk
│ ├── retrieval.py # RetrievalResult
│ ├── interview.py # InterviewQuestionSet
│ └── agent.py # WorkflowResult
├── prompts/ # Prompt 模板管理
│ ├── rag_answer.txt
│ ├── router.txt
│ ├── jd_parser.txt
│ └── resume_parser.txt
├── config/ # 配置中心
│ ├── settings.py
│ ├── retrieval.yaml
│ ├── workflow.yaml
│ ├── model.yaml
│ ├── answer.yaml
│ └── tool.yaml
├── harness/ # Harness Engineering 相关检查与验证
│ ├── checks/
│ │ ├── schema_check.py
│ │ ├── route_check.py
│ │ ├── tool_contract_check.py
│ │ └── workflow_check.py
│ ├── eval/
│ └── runner.py
├── tests/ # 测试层
│ ├── unit/
│ └── integration/
├── data/ # 原始输入数据
│ ├── jd/
│ ├── resume/
│ ├── tech_docs/
│ └── interview/
├── docs/
├── screenshots/
├── .github/
│ └── workflows/
│ └── ci.yml
├── README.md
├── requirements.txt
└── .gitignore
- Qwen Embedding:
text-embedding-v4 - Qwen Generation:
qwen-plus - Dense Retrieval
- BM25 Retrieval
- Hybrid Retrieval
- Prompt Engineering
- Citation-Grounded Answering
- Config-driven Router
- Priority-based Routing
- Tool Registry
- Tool Contract
- Rule-based Tools
- Agent Workflow
- WorkflowResult 统一输出
- Python
- FastAPI
- Streamlit
- Pydantic
- PyTest
- Git / GitHub
- GitHub Actions CI
- YAML Config
- PyMuPDF
- TXT / Markdown / PDF 文档加载
推荐使用 Python 3.10。
python -m venv .venvWindows 激活:
.venv\Scripts\activateLinux / Mac 激活:
source .venv/bin/activatepip install -r requirements.txtWindows PowerShell:
$env:DASHSCOPE_API_KEY="你的APIKey"Linux / Mac:
export DASHSCOPE_API_KEY="你的APIKey"如果只测试 bm25 模式,则不需要配置 Qwen API Key。
在 config/retrieval.yaml 中设置:
retrieval_mode: bm25可选值:
dense / bm25 / hybrid
完整示例:
chunk_size: 500
chunk_overlap: 100
# 可选: dense / bm25 / hybrid
retrieval_mode: bm25
embedding_model: text-embedding-v4
embedding_dimensions: 1024
top_k: 3
batch_size: 10
normalize_embeddings: true
# hybrid retrieval 参数
hybrid_alpha: 0.6
bm25_top_k: 10
dense_top_k: 10注意:YAML 文件中每一行配置都应使用英文冒号 :,注释应以 # 开头。
streamlit run app/main.pyuvicorn backend.main:app --reload默认访问:
http://127.0.0.1:8000
将原始数据放入以下目录:
data/jd/:岗位 JDdata/resume/:简历 PDF / TXTdata/tech_docs/:技术文档、学习笔记data/interview/:面试题、面经、八股资料
当前支持的文档类型:
.txt.md.pdf
建议至少准备:
- 1 份岗位 JD
- 1 份简历
- 1 份技术文档
- 1 份面试题资料
在 Streamlit 的 RAG 问答 tab 中输入问题,例如:
这个岗位主要要求哪些技能?
调用情况取决于 retrieval_mode:
| retrieval_mode | 调用 Qwen Embedding | 调用 Qwen Generation |
|---|---|---|
dense |
是 | 是 |
bm25 |
否 | 是 |
hybrid |
是 | 是 |
说明:
bm25模式下检索不调用 Qwen Embedding;- 只要进入回答生成阶段,就会调用 Qwen
qwen-plus; - 如果触发拒答,则不会调用生成模型。
在 JD/简历匹配 tab 中分别输入岗位 JD 和简历文本。
输出包括:
- 岗位解析结果
- 简历解析结果
- 匹配技能
- 缺失技能
- 修改建议
- 匹配分数
该模块当前为规则版工具,通过 Agent Workflow 调用,不调用 Qwen,不消耗 token。
在 面试题生成 tab 中输入岗位 JD 和简历文本。
输出包括:
- 基础问题
- 技能问题
- 差距追问
- 项目问题
该模块当前为规则版工具,通过 Agent Workflow 调用,不调用 Qwen,不消耗 token。
在 Router 调试 tab 中输入用户请求,可以查看该请求会被路由到哪个任务。
例如:
帮我分析我的简历和这个岗位是否匹配
预期路由结果:
skill_matcher
该模块不调用 Qwen,不消耗 token。
先在 config/retrieval.yaml 中设置:
retrieval_mode: bm25然后运行:
from rag.pipeline import prepare_retriever
retriever = prepare_retriever("data", chunk_size=200, chunk_overlap=50)
results = retriever.retrieve("这个岗位要求哪些技能", top_k=3)
for r in results:
print(r.model_dump())该流程不调用 Qwen Embedding,不消耗 embedding token。
先在 config/retrieval.yaml 中设置:
retrieval_mode: hybrid然后运行:
from rag.pipeline import prepare_retriever
retriever = prepare_retriever("data", chunk_size=200, chunk_overlap=50)
results = retriever.retrieve("这个岗位要求哪些技能", top_k=3)
for r in results:
print(r.model_dump())该流程会调用 Qwen Embedding,会消耗 token。
from rag.pipeline import answer_query
result = answer_query("这个岗位主要要求哪些技能?", "data", top_k=3)
print(result.model_dump())注意:该流程可能会调用 Qwen Embedding 与 Qwen Generation,是否调用 embedding 取决于 retrieval_mode。
from agent.router import route_query
print(route_query("帮我分析我的简历和这个岗位是否匹配"))
print(route_query("根据我的简历和岗位 JD 生成面试题"))
print(route_query("帮我解析这份简历"))该流程不调用 Qwen,不消耗 token。
from tools.skill_matcher import match_skills
jd_text = "岗位要求:熟悉 Python、RAG、FastAPI、Docker。"
resume_text = "技能:Python, RAG, FastAPI。"
result = match_skills(jd_text, resume_text)
print(result.model_dump())该流程不调用 Qwen,不消耗 token。
from agent.workflow import run_workflow
result = run_workflow(
query="我的简历和岗位匹配吗",
jd_text="岗位要求:熟悉 Python、RAG、FastAPI。",
resume_text="技能:Python, RAG。"
)
print(result.model_dump())该流程不调用 Qwen,不消耗 token。
pytest tests/unit -v单元测试主要验证 schema、loader、chunker、router、tools、workflow、BM25、answer logic 等本地逻辑,默认不调用 Qwen,不消耗 token。
通过以下目录划分系统职责:
rag/:负责文档问答主链路agent/:负责路由和工具编排tools/:负责具体可调用工具schemas/:负责输入输出结构约束prompts/:负责 prompt 模板管理config/:负责系统行为配置tests/:负责核心逻辑验证harness/:负责质量检查与后续评测
项目通过以下方式降低 AI 系统不确定性:
- Pydantic Schema 约束结构化输出
- Prompt 模板外置,避免散落在代码中
- Router 规则配置化,避免硬编码
- Retrieval mode 配置化,避免检索策略写死
- Tool Contract 声明工具输入和输出结构
- WorkflowResult 统一 Agent Workflow 输出
- 单元测试覆盖核心本地逻辑
- 后续计划增加 benchmark / regression harness
项目通过以下方式增强可维护性:
- 使用
config/*.yaml管理参数 - 使用
config/tool.yaml管理工具契约 - 使用
config/retrieval.yaml管理检索策略 - 使用 Git 记录开发过程
- 使用 GitHub Actions 执行基础 CI
- 后续计划引入更完整的 checks 与 regression tests
当前 router 已支持 priority-based routing,但本质上仍是规则路由。
后续计划:
- 引入 scoring router
- 支持规则置信度计算
- 当规则置信度较低时,调用 Qwen 做 LLM router fallback
- 支持复杂任务的 multi-step planner
当前 JD Parser、Resume Parser、Skill Matcher、Interview Question Generator 均为规则版。
优点:
- 稳定
- 可测试
- 不消耗 token
- 工程可控
限制:
- 对自然语言表达泛化能力有限
- 对复杂 JD / 简历解析能力较弱
- 面试题生成仍偏模板化
后续计划:
- 引入 Qwen-based JD Parser
- 引入 Qwen-based Resume Parser
- 引入 Qwen-based Interview Question Generator
- 保留 rule-based tools 作为低成本稳定 baseline
当前 Hybrid Retrieval 已完成基础框架,但仍属于早期版本。
当前特点:
- Dense 与 BM25 分数使用 min-max normalization
- 使用
hybrid_alpha加权融合 - 暂未接入 reranker
- 暂未完成 Recall@K / MRR 评测
后续计划:
- 构建 retrieval benchmark
- 对 dense / BM25 / hybrid 进行 Recall@K 与 MRR 对比
- 接入 Reranker
- 优化中英文混合 tokenizer
- 优化 hybrid score calibration
- 优化 JD Parser
- 优化 Resume Parser
- 优化 Skill Matcher
- 优化 Interview Question Generator
- Scoring Router
- LLM Router Fallback
- Multi-step Planner
- 工具调用结果校验
- Retrieval Benchmark
- Recall@K / MRR
- Reranker
- Hybrid score calibration
- Schema checks
- Prompt checks
- Tool contract checks
- Retrieval regression benchmark
- CI quality gate
- 增加 demo screenshots
- 增加 sample data
- 增加项目架构图
- 增加部署说明
相比普通的 RAG Demo,本项目更强调:
- 场景化:围绕岗位 JD、简历与技术文档的真实求职场景设计
- 工程化:通过模块划分、Schema、配置、测试与 CI 提高可维护性
- 可控性:通过 Harness Engineering 思路约束 AI 输出行为
- 可扩展性:通过 Retrieval Mode、Agent Router、Tool Contract 与 WorkflowResult 支持后续多任务扩展
- 可展示性:兼具 GitHub 项目展示、简历项目描述与面试讲解价值
当前仅用于个人学习、项目展示与求职场景实践,后续可根据需要补充正式 License。