JobHuntAgent:本地 LLM 驱动的求职自动化智能体
CanonTaito/JobHuntAgent
GitHub 项目 CanonTaito/JobHuntAgent 是一个求职自动化智能体,可扫描职位描述、按候选人画像打分、推断隐藏薪资区间并追踪申请,提交前需人工确认。当前处于 Phase 2,用本地 LLM(默认 ollama 的 gemma3)分析,结果持久化到 SQLite,匹配打分需工具调用能力模型如 qwen2.5:3b。所有提示词经占位符脱敏,真实 PII 仅存本地。
A job-search automation agent: it searches job listings, matches them against a candidate profile, infers hidden salary ranges, shortlists the strongest fits, and tracks applications end-to-end — with human sign-off before anything is ever submitted.
Current status: Phase 2 — scan a job description into a structured breakdown, score it against your candidate profile, and persist discovered listings to SQLite. A local LLM does the analysis.
The full roadmap lives in PLAN.md.
Stack
| Piece | Choice |
|---|---|
| Backend | ASP.NET Core Minimal API (.NET 10) |
| Agent orchestration | Microsoft Agent Framework (MAF) on Microsoft.Extensions.AI |
| Chat provider | ollama (default) or OpenCode Zen — one config switch; both speak the OpenAI API via Microsoft.Extensions.AI.OpenAI |
| Frontend | React 19 + TypeScript (Vite), oxlint |
| Persistence | EF Core + SQLite |
Prerequisites
-
.NET SDK 10.0.401 (pinned in
global.json) -
Node.js 20.19+ or 22.12+ (verified on 22.16.0)
-
Ollama running locally, with the chat model pulled:
ollama pull gemma3
Quick start
# API — http://localhost:5051 dotnet run --project src/JobHunt.Api --urls http://localhost:5051 # Web — http://localhost:5173 (proxies /api to the API above) npm --prefix src/JobHunt.Web install npm --prefix src/JobHunt.Web run dev
Open http://localhost:5173, paste a job description, and hit Scan job description. On a CPU-only machine with gemma3, expect roughly a minute per scan.
Scanning and tailoring work with any chat model. Match scoring and discovery call agent tools, so they need a tool-capable model — e.g. qwen2.5:3b:
AI__Ollama__ChatModel=qwen2.5:3b dotnet run --project src/JobHunt.Api
Try the API directly:
curl -X POST http://localhost:5051/api/scan \
-H "Content-Type: text/plain" \
--data-binary @job-description.txtDiscover and score listings from the job source (query is derived from your profile preferences), inferring a salary band for each:
curl -X POST "http://localhost:5051/api/discover?max=5"Draft tailored resume bullets and a cover letter for a stored application (uses the application id returned by discover):
curl -X POST "http://localhost:5051/api/applications/1/tailor"Configuration
All settings live under the AI section of src/JobHunt.Api/appsettings.json and can be overridden with environment variables using .NET's __ separator (AI__Provider=zen).
| Setting | Default | Notes |
|---|---|---|
AI:Provider |
ollama |
ollama or zen |
AI:Ollama:Endpoint |
http://localhost:11434/v1 |
ollama's OpenAI-compatible endpoint |
AI:Ollama:ChatModel |
gemma3 |
any chat model ollama has pulled |
AI:Ollama:EmbeddingModel |
nomic-embed-text |
used from Phase 1 for match scoring |
AI:Zen:Endpoint |
https://opencode.ai/zen/v1 |
|
AI:Zen:ChatModel |
big-pickle |
|
AI:Zen:ApiKey |
— | from OPENCODE_ZEN_KEY in .env |
Using OpenCode Zen instead of ollama
-
Copy
.env.exampleto.env(gitignored) and set your key:OPENCODE_ZEN_KEY=sk-...
-
Switch the provider for one run:
AI__Provider=zen dotnet run --project src/JobHunt.Api
On PowerShell:
$env:AI__Provider = "zen"; dotnet run --project src/JobHunt.Api
Caveat: Zen's free tier only answers calls made from inside opencode. External apps need a funded Console account, so ollama stays the default.
The API also walks up from src/JobHunt.Api looking for a .env, so the key is picked up without extra setup.
Candidate profile
The agent scores jobs against a candidate profile (Phase 1+). A fictional sample ships in the repo:
- Default:
src/JobHunt.Api/profile.sample.jsonis loaded when nothing else is found. - Yours: copy it to
src/JobHunt.Api/profile.jsonand edit it —profile*.jsonis gitignored except the sample, so your details never enter git. - Anywhere: or set
Profile:Path(env:Profile__Path) to any absolute or content-root-relative path.
Inspect what loaded: GET http://localhost:5051/api/profile. Only name is required — every other field is optional and profession-agnostic.
Privacy
No personally identifiable information ever reaches the chat or embedding model. Starting with Phase 1, every prompt is built from placeholder-substituted text (e.g. [NAME], [EMPLOYER_1]): the real values and the placeholder mapping are kept only locally in SQLite (see Database) and re-substituted when responses come back. The known PII values — name (plus its first/last tokens), employers, institutions, and the profile's location — are replaced wherever they appear in profile text, including free-text fields. See exactly what the model would receive via GET /api/profile/redacted. With the default ollama provider, everything stays on your machine. Job ads are public content and pass through unchanged.
Database
Phase 2 adds SQLite persistence via EF Core. The database file lives next to the API project at src/JobHunt.Api/jobhunt.db (gitignored — it holds the PII placeholder mapping) and migrations apply automatically on startup. Point it elsewhere with ConnectionStrings:JobHunt (env: ConnectionStrings__JobHunt); a relative Data Source is resolved against the API's content root.
Two tables:
| Table | Holds |
|---|---|
Applications |
discovered/scored listings, including SalaryBand, SalarySource (Displayed/Inferred), Status, the match scores (OverallScore, SkillsScore, …), and the tailored draft (TailoredBullets, CoverLetter, TailoredAt) |
PiiMappings |
the placeholder ↔ real-value map from the privacy rule above — real values never leave this table, only placeholders reach the model |
After changing the model, add a migration:
dotnet ef migrations add <Name> --project src/JobHunt.Api --startup-project src/JobHunt.Api --output-dir Features/Persistence/Migrations
Salary bands
Many ads say "Salary undisclosed" yet still carry a hidden range used only to power search filters. The engine labels every listing with a band and records how it was found (SalarySource):
Displayed— the ad states a salary; the band comes straight from it.Inferred— no salary shown, so the engine probes the source's salary filter bucket by bucket; the ad silently drops out once the filter crosses its hidden range, revealing the band it sits in.Unknown— nothing could be recovered.
Bands: Under 80k · 80-99k · 100-119k · 120-149k · 150-199k · 200k+. Run POST /api/discover and each new application is persisted with its inferred band; the tracker (Phase 2.7) will group by it.
Endpoints
| Method | Route | Purpose |
|---|---|---|
GET |
/ |
liveness |
GET |
/api/ai/ping |
round-trips a trivial prompt through the configured chat model |
GET |
/api/profile |
the loaded candidate profile (contains your PII — for local verification) |
GET |
/api/profile/redacted |
the same profile with PII replaced by placeholders — what the LLM sees |
POST |
/api/scan |
scans a raw job description (request body = text) and returns the structured analysis |
POST |
/api/match |
scores a raw job description against the profile (request body = text) — overall, embedding similarity, and the LLM breakdown |
POST |
/api/discover |
discovers listings from the job source, scores each new one, infers its salary band, and persists it; ?max=N caps how many (default 5) |
POST |
/api/applications/{id}/tailor |
drafts tailored resume bullets and a cover letter for a stored application and persists them (status → Tailored); 404 if the id is unknown |
Layout
src/
JobHunt.Api/ ASP.NET Core Minimal API
Ai/ config-switchable chat + embedding wiring, .env loader
Features/Applications/ the JobApplication entity + status enum
Features/Discovery/ discovery → score → persist pipeline
Features/JdScan/ the JD Scanner agent + result model + JSON parser
Features/Jobs/ IJobSource connector seam + in-memory source
Features/Match/ GetProfile + ScoreJob agent tools for match scoring
Features/Persistence/ EF Core SQLite context, migrations, registration
Features/Pii/ PII placeholder engine (redact out, rehydrate back)
Features/Profile/ candidate profile model + startup loader
Features/Salary/ salary-band model + displayed/inferred band inference engine
Features/Tailoring/ tailoring agent (resume bullets + cover letter) and service
Program.cs endpoints + DI composition root
profile.sample.json fictional sample profile (profile.json is yours, gitignored)
JobHunt.Web/ React + Vite frontend
src/scan.ts typed client for POST /api/scan
src/ScanPage.tsx the Scan page
src/match.ts typed client for POST /api/match
src/MatchPage.tsx the Match page with the score breakdown card
PLAN.md phased build plan
global.json pins the .NET SDK version
Development
dotnet build JobHuntAgent.slnx -warnaserror npm --prefix src/JobHunt.Web run lint npm --prefix src/JobHunt.Web run build
Roadmap
Phases 1–4 in PLAN.md: match scoring against a candidate profile, a discovery → scoring → tailoring → approval → tracking pipeline with salary-band inference, outcome memory, then live SEEK search, OpenTelemetry, and a test suite.
来源:GitHub · github.com