feat: AI 择业顾问 MVP — 专业分析 + 岗位匹配 + 多轮对话

- backend: career-advice module with analyze/chat/positions endpoints
- frontend: career.vue page with profile form, AI advice, recommendation cards
- config/api/pages/user.vue: full integration into existing flow
- docs: PROJECT-STATUS v4.5, FEATURE-LIST v4.3, ROADMAP v4.3
- AGENTS.md: updated module count and career link paths
This commit is contained in:
wlt
2026-06-17 10:32:23 +08:00
parent 4cd889c081
commit a5c4bcb821
13 changed files with 788 additions and 240 deletions
+2
View File
@@ -27,6 +27,7 @@ import { TtsModule } from './modules/tts/tts.module'
import { PricingModule } from './modules/schemas/pricing.module'
import { ShareModule } from './modules/share/share.module'
import { InterviewReviewModule } from './modules/interview-review/interview-review.module'
import { CareerAdviceModule } from './modules/career-advice/career-advice.module'
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin'
@@ -62,6 +63,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin
PricingModule,
ShareModule,
InterviewReviewModule,
CareerAdviceModule,
],
providers: [
JwtStrategy,
@@ -0,0 +1,37 @@
import { Controller, Post, Get, Body } from "@nestjs/common"
import { Public } from "../../common/decorators/public.decorator"
import { CareerAdviceService, CareerProfile, ChatMessage } from "./career-advice.service"
import { CurrentUser } from "../../common/decorators/current-user.decorator"
@Controller("career-advice")
export class CareerAdviceController {
constructor(private service: CareerAdviceService) {}
@Post("analyze")
async analyze(
@Body() profile: CareerProfile,
@CurrentUser("userId") userId: string,
) {
if (!profile.major || !profile.major.trim()) {
return { error: "请填写你的专业" }
}
return this.service.analyze(profile)
}
@Post("chat")
async chat(
@Body() body: { message: string; history: ChatMessage[] },
@CurrentUser("userId") userId: string,
) {
if (!body.message || !body.message.trim()) {
return { error: "请输入消息" }
}
return this.service.chat(body.message, body.history || [])
}
@Get("positions")
@Public()
async positions() {
return this.service.getHotPositions()
}
}
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common'
import { MongooseModule } from '@nestjs/mongoose'
import { CareerAdviceController } from './career-advice.controller'
import { CareerAdviceService } from './career-advice.service'
import { HotPosition, HotPositionSchema } from '../positions/positions.schema'
import { AiModule } from '../ai/ai.module'
@Module({
imports: [
MongooseModule.forFeature([
{ name: HotPosition.name, schema: HotPositionSchema },
]),
AiModule,
],
controllers: [CareerAdviceController],
providers: [CareerAdviceService],
})
export class CareerAdviceModule {}
@@ -0,0 +1,163 @@
import { Injectable, Logger } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'
import { AiService } from '../ai/ai.service'
import { HotPosition } from '../positions/positions.schema'
export interface CareerProfile {
major: string
grade?: string
interests?: string
gpa?: string
goal?: string
}
export interface CareerPath {
name: string
reason: string
matchScore: number
salary?: string
}
export interface ChatMessage {
role: 'user' | 'assistant'
content: string
}
@Injectable()
export class CareerAdviceService {
private readonly logger = new Logger(CareerAdviceService.name)
constructor(
@InjectModel(HotPosition.name) private positionModel: Model<HotPosition>,
private aiService: AiService,
) {}
/**
* Analyze user profile and return career advice with position recommendations.
*/
async analyze(profile: CareerProfile) {
const positions = await this.positionModel.find({ active: true })
.sort({ sort: 1 })
.lean()
.exec()
const positionsContext = positions.map(p =>
`- ${p.name}${p.salary ? ` (薪资: ${p.salary})` : ''}${p.company ? ` - ${p.company}` : ''}`
).join('\n')
const systemPrompt = `你是一位资深的中国大学生职业规划顾问。你的任务是根据学生的专业背景和个人情况,给出个性化的择业建议。
你的建议需要涵盖:
1. 该专业的典型职业方向和发展路径
2. "考研vs就业vs考公"的决策分析(基于学生具体情况)
3. 当前就业市场的真实形势(如AI对各行业的影响)
4. 具体可行动的建议(实习、技能提升等)
你需要从以下可用岗位中,推荐最匹配该学生的3-5个方向:
${positionsContext}
注意:
- 你的回答要诚恳务实,不要画大饼
- 指出每个选择的利弊和风险
- 结合AI时代对各行业的影响给出建议
- 最后以JSON格式输出推荐岗位列表
回复格式:
先输出一段详细的个性化分析建议(中文,300-500字)。
然后在最后单独一行输出JSON数组(不要任何其他内容):
---JSON---
[{"name":"岗位名","reason":"推荐理由简要说明","matchScore":85,"salary":"薪资范围"},...]
---JSON---`
const userMessage = this.buildProfileMessage(profile)
const rawReply = await this.aiService.call({
systemPrompt,
userMessage,
temperature: 0.7,
maxTokens: 2048,
})
return this.parseResponse(rawReply, positions)
}
/**
* Continue a chat conversation about career choices.
*/
async chat(message: string, history: ChatMessage[]) {
const positions = await this.positionModel.find({ active: true })
.sort({ sort: 1 })
.lean()
.exec()
const positionsContext = positions.map(p =>
`- ${p.name}${p.salary ? ` (薪资: ${p.salary})` : ''}`
).join('\n')
const systemPrompt = `你是一位资深的中国大学生职业规划顾问。继续与学生的对话,回答他们的择业相关问题。
可推荐的岗位列表:
${positionsContext}
注意:
- 回答诚恳务实,结合AI时代背景
- 给出具体可操作的建议
- 如果学生提到具体岗位,可以从推荐列表中选择匹配的
- 保持对话亲切自然,用中文回复`
const historyMessages = history.map(h =>
`${h.role === 'user' ? '学生' : '顾问'}: ${h.content}`
).join('\n')
const userMessage = `对话历史:\n${historyMessages}\n\n学生最新提问: ${message}`
const reply = await this.aiService.call({
systemPrompt,
userMessage,
temperature: 0.7,
maxTokens: 1024,
})
return { reply }
}
private buildProfileMessage(profile: CareerProfile): string {
const parts: string[] = [`学生专业: ${profile.major}`]
if (profile.grade) parts.push(`年级: ${profile.grade}`)
if (profile.interests) parts.push(`兴趣方向: ${profile.interests}`)
if (profile.gpa) parts.push(`GPA/成绩: ${profile.gpa}`)
if (profile.goal) parts.push(`目标/困惑: ${profile.goal}`)
return parts.join('\n')
}
private parseResponse(rawReply: string, allPositions: any[]): {
reply: string
careerPaths: CareerPath[]
} {
// Extract JSON array from ---JSON--- markers
const jsonMatch = rawReply.match(/---JSON---\s*(\[[\s\S]*?\])\s*---JSON---/)
let careerPaths: CareerPath[] = []
if (jsonMatch) {
try {
careerPaths = JSON.parse(jsonMatch[1])
} catch {
this.logger.warn('Failed to parse career paths JSON')
}
}
// Clean reply (remove --JSON-- markers)
const reply = rawReply.replace(/---JSON---[\s\S]*?---JSON---/g, '').trim()
return { reply, careerPaths }
}
/** Get hot positions (delegated from positions module) */
async getHotPositions() {
return this.positionModel.find({ active: true })
.sort({ sort: 1 })
.lean()
.exec()
}
}