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:
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user