跳到正文
GitHub·· 6 天前AI 评分28

JobHuntAgent:本地 LLM 驱动的求职自动化智能体

CanonTaito/JobHuntAgent

AI 导读

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.txt

Discover 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

  1. Copy .env.example to .env (gitignored) and set your key:

    OPENCODE_ZEN_KEY=sk-...
  2. 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.json is loaded when nothing else is found.
  • Yours: copy it to src/JobHunt.Api/profile.json and edit it — profile*.json is 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