-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathguide_agent.py
More file actions
2927 lines (2320 loc) · 124 KB
/
Copy pathguide_agent.py
File metadata and controls
2927 lines (2320 loc) · 124 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import json
import logging
import time
import random
from enum import Enum
from typing import List, Dict, Optional, Any, Union
from openai import OpenAI
from pydantic import ValidationError
from model import Task, TaskStrategy, AgentState, AgentDecision, AgentSnapshot, ExecutionDoc
from prompt_utils import inject_json_rules, inject_reply_rules, extract_json_from_markdown, heuristic_json_parse
logger = logging.getLogger(__name__)
# ==================== 错误分类系统 ====================
class ErrorType(Enum):
"""错误类型分类"""
TRANSIENT = "transient" # 临时性错误(可重试)
RATE_LIMIT = "rate_limit" # 速率限制(需要退避)
PERMANENT = "permanent" # 永久性错误(不可重试)
PARSING = "parsing" # 解析错误(使用兜底方案)
class RecoverableError(Exception):
"""
可恢复的错误(不应中断对话流,应该引导用户换个说法)
这类错误由API层统一处理,返回200 + 拟人化回复,
保持对话历史完整,引导用户换个方式表达。
"""
pass
def classify_error(error: Exception) -> ErrorType:
"""
根据错误类型分类,决定重试策略
Args:
error: 捕获的异常
Returns:
ErrorType: 错误分类枚举
"""
error_str = str(error).lower()
# 速率限制错误
if "429" in error_str or "rate limit" in error_str or "quota" in error_str:
return ErrorType.RATE_LIMIT
# 服务过载/临时不可用
if "503" in error_str or "overloaded" in error_str or "timeout" in error_str:
return ErrorType.TRANSIENT
# 连接错误(网络问题)
if "connection" in error_str or "network" in error_str or "refused" in error_str:
return ErrorType.TRANSIENT
# 永久性错误(参数错误、权限错误)
if "400" in error_str or "401" in error_str or "403" in error_str:
return ErrorType.PERMANENT
# JSON解析错误
if isinstance(error, json.JSONDecodeError):
return ErrorType.PARSING
# Pydantic验证错误
if isinstance(error, ValidationError):
return ErrorType.PARSING
# 默认为临时性错误(保守策略,允许重试)
return ErrorType.TRANSIENT
# ==================== JSON 解析错误分析与 Self-Correction ====================
class JsonErrorType(Enum):
"""JSON 解析错误类型"""
FORMAT_ERROR = "format_error" # 格式错误(可 Self-Correction)
TRUNCATED = "truncated" # 响应被截断(可能是过载)
EMPTY = "empty" # 空内容
def analyze_json_error(content: str, error: Exception) -> JsonErrorType:
"""
分析 JSON 解析失败的原因,用于决定后续处理策略
Args:
content: LLM 返回的原始内容
error: json.loads 抛出的异常
Returns:
JsonErrorType: 错误类型枚举
"""
# 空内容检测
if not content or len(content.strip()) < 10:
logger.warning(f"🔍 JSON 错误分析: 空内容或过短 (长度={len(content) if content else 0})")
return JsonErrorType.EMPTY
# 检查括号是否匹配(截断检测)
open_braces = content.count('{')
close_braces = content.count('}')
if open_braces != close_braces:
logger.warning(f"🔍 JSON 错误分析: 括号不匹配 ({{ 数量={open_braces}, }} 数量={close_braces}),判定为截断")
return JsonErrorType.TRUNCATED
# 检查是否以完整 JSON 结构结尾
stripped = content.strip()
if not stripped.endswith('}'):
logger.warning(f"🔍 JSON 错误分析: 未以 '}}' 结尾,判定为截断")
return JsonErrorType.TRUNCATED
# 检查响应长度是否异常短(可能是过载导致的不完整响应)
# 正常的 AgentDecision JSON 至少应该有 100+ 字符
if len(stripped) < 100:
logger.warning(f"🔍 JSON 错误分析: 响应长度异常短 ({len(stripped)} 字符),可能是截断")
return JsonErrorType.TRUNCATED
# 其他情况视为格式错误(可 Self-Correction)
logger.warning(f"🔍 JSON 错误分析: 格式错误,可尝试 Self-Correction")
return JsonErrorType.FORMAT_ERROR
def request_json_correction(client, model: str, original_content: str,
error_msg: str, messages: list) -> str:
"""
发送 Self-Correction 请求,让 LLM 修正 JSON 格式(已弃用)
注意:此函数在双流策略中已不再使用,由 _parse_decision_json() 替代。
保留此函数仅为向后兼容。
"""
truncated_content = original_content[:800] if len(original_content) > 800 else original_content
correction_prompt = (
f"你刚才生成的 JSON 格式有误,报错信息是:{error_msg}\n"
f"原始输出:{truncated_content}\n\n"
f"请修正格式,只返回修正后的完整 JSON(用 Markdown 代码块包裹),不要包含任何解释文字。"
)
correction_messages = messages + [
{"role": "assistant", "content": original_content},
{"role": "user", "content": correction_prompt}
]
logger.info(f"🔄 发送 Self-Correction 请求...")
response = client.chat.completions.create(
model=model,
messages=correction_messages,
stream=False,
timeout=30.0
)
corrected_content = response.choices[0].message.content
logger.info(f"🔄 Self-Correction 响应 (长度={len(corrected_content) if corrected_content else 0})")
return corrected_content
class GuideAgent:
"""
简历优化引导 Agent (状态机驱动)
负责通过对话挖掘信息、生成草稿、请求用户确认,最终输出ExecutionDoc。
状态机流程:
DISCOVERY -> DRAFTING -> CONFIRMING -> FINISHED
"""
def __init__(self, task: Task, context: Optional[Dict[str, Any]] = None):
self.task = task
self.context = context or {} # 任务流转上下文(跳过的任务、进度等)
# 核心状态 (Single Source of Truth)
self.current_state = AgentState.DISCOVERY # 当前流程状态
self.messages: List[Dict] = [] # 完整的对话历史
self.draft: Optional[str] = None # 当前持有的最新草稿
self.execution_doc: Optional[ExecutionDoc] = None # 待确认的执行文档
# 从统一配置模块读取
from config import DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL, API_TIMEOUT, API_MAX_RETRIES
# 初始化 OpenAI 客户端(配置重试策略)
self.client = OpenAI(
base_url=DEEPSEEK_BASE_URL,
api_key=DEEPSEEK_API_KEY,
max_retries=API_MAX_RETRIES,
timeout=API_TIMEOUT,
)
self.model = DEEPSEEK_MODEL
def _format_draft_content(self, content: str) -> str:
"""
格式化草稿内容,确保 bullet points 正确换行。
处理规则:
1. 确保每个 bullet point(•、-、*)前面有换行符
2. 将连续的 bullet points 分隔成独立行
3. 统一使用 Markdown 格式(- )
"""
if not content:
return content
import re
import json
# #region agent log
with open(r'c:\Users\admin\Desktop\ResumeAssistant\.cursor\debug.log', 'a', encoding='utf-8') as f: f.write(json.dumps({"location":"guide_agent.py:_format_draft_content:entry","message":"format_draft_content called","data":{"content_length":len(content),"content_preview":content[:200] if len(content)>200 else content,"has_bullet_u2022":"•" in content,"has_dash_space":"- " in content,"newline_count":content.count("\n")},"timestamp":__import__('time').time()*1000,"sessionId":"debug-session","hypothesisId":"A,B,C"},ensure_ascii=False)+"\n")
# #endregion
# 1. 首先统一不同的 bullet 符号为 Markdown 格式
# 处理 Unicode bullet(•)和其他常见符号
content = re.sub(r'(?<!\n)•\s*', '\n- ', content) # • 前面没有换行时添加换行
content = re.sub(r'(?<!\n)\*\s+(?=[A-Z\u4e00-\u9fff])', '\n- ', content) # * 作为列表项时
# 2. 确保 Markdown 列表项(- )前面有换行符
# 但要避免影响代码块中的减号
content = re.sub(r'(?<!\n)(?<!^)- (?=\*?\*?[A-Z\u4e00-\u9fff(【])', '\n- ', content)
# 3. 确保标题后的列表有换行
content = re.sub(r'(\*\*[^*]+\*\*[::])(?=\s*[-•])', r'\1\n', content)
# 4. 清理多余的空行(超过2个连续换行变成2个)
content = re.sub(r'\n{3,}', '\n\n', content)
# 5. 确保开头没有多余的换行
content = content.lstrip('\n')
# #region agent log
with open(r'c:\Users\admin\Desktop\ResumeAssistant\.cursor\debug.log', 'a', encoding='utf-8') as f: f.write(json.dumps({"location":"guide_agent.py:_format_draft_content:exit","message":"format_draft_content completed","data":{"content_length":len(content),"content_preview":content[:200] if len(content)>200 else content,"newline_count_after":content.count("\n")},"timestamp":__import__('time').time()*1000,"sessionId":"debug-session","hypothesisId":"A,B,C"},ensure_ascii=False)+"\n")
# #endregion
return content
def _set_draft(self, draft_content: Optional[str]):
"""统一设置草稿内容,自动进行格式化"""
import json
# #region agent log
with open(r'c:\Users\admin\Desktop\ResumeAssistant\.cursor\debug.log', 'a', encoding='utf-8') as f: f.write(json.dumps({"location":"guide_agent.py:_set_draft","message":"_set_draft called","data":{"has_draft_content":draft_content is not None,"draft_length":len(draft_content) if draft_content else 0},"timestamp":__import__('time').time()*1000,"sessionId":"debug-session","hypothesisId":"A"},ensure_ascii=False)+"\n")
# #endregion
if draft_content:
self.draft = self._format_draft_content(draft_content)
# #region agent log
with open(r'c:\Users\admin\Desktop\ResumeAssistant\.cursor\debug.log', 'a', encoding='utf-8') as f: f.write(json.dumps({"location":"guide_agent.py:_set_draft:after_format","message":"draft after formatting","data":{"draft_preview":self.draft[:300] if len(self.draft)>300 else self.draft,"newline_count":self.draft.count("\n")},"timestamp":__import__('time').time()*1000,"sessionId":"debug-session","hypothesisId":"A,D"},ensure_ascii=False)+"\n")
# #endregion
else:
self.draft = draft_content
def _get_system_prompt(self) -> str:
"""
根据当前状态和任务策略动态构建 System Prompt。
支持两种策略:STAR_STORYTELLING(深挖故事)和 KEYWORD_FILTER(技能筛选)
"""
# 获取任务策略
strategy = self.task.strategy
# 状态描述
state_description = ""
if self.current_state == AgentState.DISCOVERY:
if strategy == TaskStrategy.KEYWORD_FILTER:
state_description = (
"当前状态: DISCOVERY (技能筛选分析)\n"
"目标: 直接分析用户技能,给出筛选建议,不需要深度追问。\n"
"约束: 第一轮就直接给出分析结果(做减法+做加法),询问用户确认。\n"
"可选动作: CONTINUE_ASKING (用户需要补充信息), PROPOSE_DRAFT (给出草稿)"
)
else:
state_description = (
"当前状态: DISCOVERY (信息挖掘)\n"
"目标: 基于诊断结果,向用户提问以获取必要的信息。\n"
"约束: 暂时**不要**提供草稿。专注于理解用户的经历和细节。\n"
"可选动作: CONTINUE_ASKING, PROPOSE_DRAFT\n"
"\n"
"📊 智能节奏控制(每次回复前必做判断):\n"
"\n"
"在决定 next_action 和 reply_to_user 之前,先在 thought 中分析:\n"
"\n"
"1️⃣ **用户回复质量评估**:\n"
" - 详细型(>50字,包含具体信息)→ 说明用户有料可聊\n"
" - 简短型(10-50字,简单回答)→ 用户可能不确定说什么\n"
" - 极简型(<10字,\"嗯\"/\"是\"/\"不知道\")→ 用户可能卡住了\n"
"\n"
"2️⃣ **下一步策略选择**:\n"
"\n"
" 【详细型回复】→ 深挖模式\n"
" - 继续问当前话题的深入问题(只问1个)\n"
" - 例:\"你提到了XX,能具体说说...\"\n"
" - 保持单轮单焦点,不要一次问多个问题\n"
" \n"
" 【简短型回复】→ 引导模式\n"
" - 不要继续追问开放式问题\n"
" - 给2-3个选项让用户选择\n"
" - 或换一个更具体的话题\n"
" \n"
" 【极简型 + \"不知道/没有\"】→ 猜测模式\n"
" - 立即触发智能猜测机制(已有)\n"
" - 基于项目类型给出具体选项\n"
"\n"
"3️⃣ **话题切换信号**:\n"
" 当满足以下条件时,结束当前话题,自然过渡到下一个维度:\n"
" - 用户连续2轮回答都很简短\n"
" - 用户明确表示\"就这些\"/\"没了\"\n"
" - 当前话题已收集到关键信息\n"
" \n"
" 过渡话术:\n"
" \"好的,这块我了解了!那我们聊聊另一个方面...\"\n"
" \"明白了~换个角度,关于XX...\"\n"
"\n"
"4️⃣ **禁止行为**:\n"
" ❌ 不要在用户回答简短时继续追问同类问题\n"
" ❌ 不要一次性问多个问题(哪怕是相关话题)\n"
" ❌ 不要在用户说\"不知道\"后重复问相同问题\n"
" ✅ 每轮对话只聚焦1个话题,只问1个问题\n"
)
elif self.current_state == AgentState.DRAFTING:
state_description = (
"当前状态: DRAFTING (草稿撰写)\n"
"目标: 展示优化后的草稿,或根据用户的反馈进行修改。\n"
"\n"
"⚠️ 关键判断指引(必须在 thought 中分析):\n"
"在决定 next_action 之前,先在 thought 中分析用户的回复属于哪种情况:\n"
"1. 【认可草稿】用户对草稿表示满意,没有提出修改意见 → 使用 REQUEST_CONFIRM\n"
"2. 【修改意见】用户提出了具体的修改要求或补充内容 → 使用 PROPOSE_DRAFT\n"
"3. 【提供新信息】用户补充了新的细节信息 → 使用 PROPOSE_DRAFT 更新草稿\n"
"4. 【指出错误】用户指出草稿有错误(如内容归属错误、信息不准确)→ 使用 PROPOSE_DRAFT 修正\n"
"\n"
"语义理解要点:\n"
"- 如果你刚刚展示了草稿,用户用简短肯定词回复(如\"好\"、\"可以\"、\"行\"),\n"
" 结合上下文,这通常表示用户认可草稿,应该使用 REQUEST_CONFIRM。\n"
"- 如果用户说\"好,但是...\"或\"可以,不过...\",这表示有修改意见,使用 PROPOSE_DRAFT。\n"
"- 如果用户指出错误(如\"这不对\"、\"这是本科的不是硕士的\"、\"搞混了\"),\n"
" 这是修改意见,使用 PROPOSE_DRAFT 重新生成正确的草稿。\n"
"- 如果用户只是回应你的问题但没有表态草稿满意度,继续对话。\n"
"\n"
"🔄 用户修改意见处理流程:\n"
"1. 在 thought 中分析用户的具体修改要求\n"
"2. 根据修改要求更新草稿内容\n"
"3. 在 reply_to_user 中说明你做了哪些调整\n"
"4. 在 draft_content 中输出修改后的完整草稿\n"
"5. 使用 PROPOSE_DRAFT 展示新草稿,等待用户再次反馈\n"
"\n"
"📝 草稿展示规范(必须遵守):\n"
"当你使用 PROPOSE_DRAFT 展示草稿时,在 reply_to_user 中必须包含:\n"
"\n"
"1. **位置说明**(首次展示草稿时):\n"
" - 明确告诉用户这个草稿将应用在哪个位置\n"
" - 格式:\"我帮你优化了【板块 - 具体条目】的XXX内容\"\n"
" - 示例:\"我帮你优化了【教育背景 - 硕士阶段】的主修课程,草稿如下:\"\n"
" - 示例:\"我帮你优化了【项目经历 - DeepResearch Agent】的技术描述,草稿如下:\"\n"
"\n"
"2. **修改对比说明**(用户反馈后重新生成草稿时):\n"
" - 对比说明之前的问题和现在的修正\n"
" - 格式:\"❌ 之前XXX(问题),✅ 现在改成XXX(修正)\"\n"
" - 示例:\"抱歉!我重新整理了一下:\n"
" - ❌ 之前错误:将本科阶段的课程(计算机视觉、模式识别)混入了硕士背景\n"
" - ✅ 现在修正:只保留硕士阶段的核心课程(机器学习、深度学习、数据挖掘)\n"
" \n"
" 新草稿如下:\"\n"
"\n"
"3. **禁止的错误示例**:\n"
" - ❌ \"好的,已修改。\"(没有说明修改了什么)\n"
" - ❌ \"草稿如下:\"(没有说明应用位置)\n"
" - ❌ \"我优化了内容\"(没有明确指出是哪个板块的哪个条目)\n"
"\n"
"可选动作: PROPOSE_DRAFT (继续修改), REQUEST_CONFIRM (请求确认)"
)
elif self.current_state == AgentState.CONFIRMING:
state_description = (
"当前状态: CONFIRMING (等待确认)\n"
"目标: 用户已看到预览,正在等待最终确认。\n"
"\n"
"⚠️ 关键判断指引(必须在 thought 中分析):\n"
"用户的回复属于哪种情况:\n"
"1. 【确认执行】用户认可草稿,同意应用修改 → 使用 CONFIRM_FINISH\n"
"2. 【要求修改】用户提出了修改意见或指出错误 → 使用 PROPOSE_DRAFT\n"
"\n"
"语义理解要点:\n"
"- 在确认阶段,用户的简短肯定回复(如\"好\"、\"确认\"、\"没问题\"、\"就这样\")\n"
" 通常表示同意执行,应该使用 CONFIRM_FINISH。\n"
"- 如果用户说\"再改改\"、\"还要调整\"、\"这不对\"、\"搞错了\"等,则使用 PROPOSE_DRAFT。\n"
"- 如果用户提供了具体的修改建议或补充信息,使用 PROPOSE_DRAFT 更新草稿。\n"
"\n"
"🔄 用户要求修改时的处理:\n"
"1. 回退到 DRAFTING 状态\n"
"2. 根据用户反馈修正草稿\n"
"3. 展示新草稿,等待再次确认\n"
"\n"
"可选动作: CONFIRM_FINISH (用户确认), PROPOSE_DRAFT (用户要求修改)"
)
elif self.current_state == AgentState.FINISHED:
state_description = (
"当前状态: FINISHED (任务已执行完成)\n"
"背景: 用户之前确认了草稿,修改已经应用到简历中。\n"
"但用户现在又发来了消息,你需要智能理解用户的意图。\n"
"\n"
"⚠️ 关键判断指引(必须在 thought 中仔细分析):\n"
"用户的回复属于哪种情况:\n"
"\n"
"1. 【不满意/想修改】用户对刚才的修改结果不满意,想要重新调整\n"
" 信号词:\"不对\"、\"错了\"、\"不行\"、\"再改改\"、\"重新来\"、\"不满意\"、\"有问题\"\n"
" → intent 设为 BACKTRACK\n"
" → 如果用户没指明具体板块,target_section 留空(系统会默认回到当前任务)\n"
" → 如果用户指明了板块(如\"硕士那栏\"、\"技能部分\"),设置对应的 target_section\n"
" → 用自然友好的语气回应,如\"好的,我理解了。让我们重新调整一下这部分...\"\n"
"\n"
"2. 【想修改其他已完成任务】用户想回到之前完成的其他任务进行修改\n"
" 信号词:\"之前那个\"、\"刚才的XX\"、\"上面的\"、\"前面的\" + 板块名\n"
" → intent 设为 BACKTRACK\n"
" → target_section 设为用户提到的板块名称\n"
" → 友好回应,如\"没问题,我们回到那部分继续调整!\"\n"
"\n"
"3. 【满意/感谢/无关话题】用户表示满意、感谢、或聊其他话题\n"
" 信号词:\"谢谢\"、\"好的\"、\"不错\"、\"满意\"、\"完成了\"、或其他无关话题\n"
" → intent 设为 CONTINUE\n"
" → 礼貌回应,如\"不客气!如果之后还想调整任何部分,随时告诉我。\"\n"
"\n"
"🔔 重要:你的回复必须自然、友好,像一个真正理解用户的助手。\n"
"绝对不要机械地说\"任务已完成\"这种生硬的话。\n"
"\n"
"可选动作: CONTINUE_ASKING (继续对话), PROPOSE_DRAFT (重新修改)"
)
# 根据策略生成首次对话指引
first_message_instruction = ""
if len(self.messages) == 0:
if strategy == TaskStrategy.KEYWORD_FILTER:
first_message_instruction = self._get_keyword_filter_first_message_instruction()
elif strategy == TaskStrategy.CAREER_PIVOT:
first_message_instruction = self._get_career_pivot_first_message_instruction()
elif strategy == TaskStrategy.CAREER_PIVOT_SKILL:
first_message_instruction = self._get_career_pivot_skill_first_message_instruction()
else:
first_message_instruction = self._get_star_storytelling_first_message_instruction()
# 根据策略生成策略指导
strategy_instruction = ""
if strategy == TaskStrategy.KEYWORD_FILTER:
strategy_instruction = self._get_keyword_filter_strategy()
elif strategy == TaskStrategy.CAREER_PIVOT:
strategy_instruction = self._get_career_pivot_strategy()
elif strategy == TaskStrategy.CAREER_PIVOT_SKILL:
strategy_instruction = self._get_career_pivot_skill_strategy()
else:
strategy_instruction = self._get_star_storytelling_strategy()
# 精简的 AgentDecision schema(仅逻辑流)
decision_schema = {
"type": "object",
"properties": {
"intent": {
"type": "string",
"enum": ["CONTINUE", "BACKTRACK"],
"description": "用户意图:CONTINUE继续当前任务,BACKTRACK回溯到历史任务"
},
"next_action": {
"type": "string",
"enum": ["CONTINUE_ASKING", "PROPOSE_DRAFT", "REQUEST_CONFIRM", "CONFIRM_FINISH"],
"description": "下一步动作"
},
"target_section": {"type": "string", "description": "回溯目标板块名称(当intent为BACKTRACK时必填)"},
"draft_content": {"type": "string", "description": "优化后的草稿内容(当 next_action 为 PROPOSE_DRAFT 或 REQUEST_CONFIRM 时必填)"}
},
"required": ["intent", "next_action"]
}
# 构建任务流转上下文
workflow_context = self._build_workflow_context()
# 构建基础 Prompt
base_prompt = f"""
# Role
你是一位拥有15年经验的简历专家。
你的目标是优化用户简历中的特定部分,通过对话挖掘信息,最终生成高质量的优化内容。
# Context
任务 ID: {self.task.id}
简历板块: {self.task.section}
任务策略: {strategy.value}
原始文本: "{self.task.original_text}"
诊断问题: "{self.task.diagnosis}"
优化目标: "{self.task.goal}"
{workflow_context}
# State Machine Instructions
你是由一个状态机驱动的 Agent。
{state_description}
{first_message_instruction}
# Output Format(双流输出)
你的回复必须包含两部分:**UI 流**(用户可见的回复)和**逻辑流**(决策 JSON)。
详细的输出规范请参考后面的 "<reply> 标签输出规范" 和 "JSON 输出规范" 章节。
### JSON Schema
{json.dumps(decision_schema, indent=2, ensure_ascii=False)}
{strategy_instruction}"""
# 先注入 reply 标签规则,再注入 JSON 规则
prompt_with_reply = inject_reply_rules(base_prompt)
prompt_with_both = inject_json_rules(prompt_with_reply, decision_schema)
return prompt_with_both
def _build_workflow_context(self) -> str:
"""
构建任务流转上下文信息,用于让 LLM 感知用户跳过了哪些任务。
这样 LLM 可以生成更自然、更有同理心的过渡话术。
同时包含已完成任务列表,用于支持智能任务回溯。
"""
if not self.context:
return ""
skipped_tasks = self.context.get("skipped_tasks", [])
completed_tasks = self.context.get("completed_tasks", []) # 已完成的任务列表
progress = self.context.get("progress", {})
is_first_after_skip = self.context.get("is_first_after_skip", False)
context_lines = ["\n# 任务流转上下文"]
# 添加进度信息
if progress:
total = progress.get("total_tasks", 0)
completed = progress.get("completed_tasks", 0)
skipped = progress.get("skipped_tasks", 0)
context_lines.append(f"当前进度:已完成 {completed}/{total},已跳过 {skipped}/{total}")
# 添加已完成的任务信息(用于回溯识别)
if completed_tasks:
context_lines.append(f"\n**已完成的任务(可回溯修改)**:")
for task_info in completed_tasks:
context_lines.append(f"- {task_info}")
# 添加跳过的任务信息
if skipped_tasks:
context_lines.append(f"\n用户在本次会话中跳过了以下任务:{', '.join(skipped_tasks)}")
# 添加特殊提示
if is_first_after_skip:
context_lines.append("")
context_lines.append("⚠️ 用户刚刚跳过了上一个任务。这可能意味着:")
context_lines.append("- 用户觉得那个内容暂时不重要")
context_lines.append("- 用户没有相关信息可以补充")
context_lines.append("- 用户希望快速推进流程")
context_lines.append("")
context_lines.append("请用轻松友好的语气开始当前任务,不要让用户感到压力。")
context_lines.append("避免说\"你好!我们现在来优化...\"这种生硬的开场白。")
context_lines.append("可以说\"没问题!我们先看看这个部分...\"或\"好的,那我们来看这个!...\"")
return "\n".join(context_lines) + "\n"
def _get_star_storytelling_first_message_instruction(self) -> str:
"""STAR_STORYTELLING 策略的首次对话指引"""
# 检查是否是跳过任务后的第一次对话
is_first_after_skip = self.context.get("is_first_after_skip", False)
if is_first_after_skip:
return f"""
# 🚨 首次对话指引 (用户刚跳过了上一个任务)
用户刚刚跳过了一个任务,现在开始当前任务。请用轻松友好的方式开场,避免给用户压力。
**🔑 核心原则:循序渐进,只问1个问题!**
## 开场白规则:
1. **简短过渡 + 1句观察 + 1个问题**
✅ "没问题!我看了你的**{self.task.section}**,[1句观察]。[1个问题]"
2. **禁止的开场白:**
❌ "你好!我们现在来优化..."(太生硬)
❌ 列出多个问题(1. 2. 3. ...)
❌ 展示完整的问题诊断
3. **语气要求**:
- 轻松的过渡语(如"没问题"、"好的"、"OK")
- 自然的观察("我注意到..."、"我看了一下...")
- 只问1个最核心的探索性问题
## 示例(推荐格式):
\"\"\"
没问题!我看了你的**{self.task.section}**,这块还挺有提升空间的。
先问个基础问题:这个项目你是自己从头做的,还是跟着教程做的?
\"\"\"
或者:
\"\"\"
好的!那我们来看看**{self.task.section}**这部分。我注意到描述比较简略。
想先了解一下:你在这段经历中主要负责什么工作?
\"\"\"
"""
return f"""
# 🚨 首次对话指引 (STAR_STORYTELLING 模式)
这是此任务的第一次对话。请主动开场,用友好、自然的方式引导用户。
**🔑 核心原则:循序渐进引导,不要一次性抛出所有信息!**
## 开场白规则(必须遵守):
1. **禁止一次性抛出多个问题**
❌ 不要列问题清单(1. 2. 3. ...)
✅ 只问1个最核心的探索性问题
2. **禁止展示完整诊断**
❌ 不要详细解释发现的所有问题
✅ 只说1句核心观察(如"技术细节比较简略")
3. **开场问题优先级**(学生项目):
第一优先:项目熟悉程度(自己做 vs 跟着做)
- "这个项目你是自己从头做的,还是跟着教程/参考别人的?"
第二优先:实际动手部分(改了哪些代码)
- "你实际动手改了哪些部分?"
4. **语气要求**:
- 轻松、友好,像学长/学姐在聊天
- 用"我看了..."、"挺有意思"等自然表达
- 避免"您好!我们现在来优化..."这种客服式开场
## 开场消息结构(精简版):
**第1句**:简短寒暄 + 核心观察
"我看了你的**{self.task.section}**,挺有意思的!不过我注意到[1句核心观察]。"
**第2句**:提出1个最重要的问题
"先问个基础问题:这个项目你是自己从头做的,还是跟着教程/参考别人的?"
## 禁止内容:
- ❌ 不要展示完整的问题诊断
- ❌ 不要列举优化目标
- ❌ 不要给ABC选项(这也是一次性抛出太多)
- ❌ 不要问"背景、职责、难点、结果"等STAR问题
- ✅ 这些内容会在后续对话中根据用户回答逐步展开
## 示例(推荐格式):
\"\"\"
我看了你的**{self.task.section}**,挺有意思的!不过我注意到技术细节这块可以再丰富一些。
先问个基础问题:这个项目你是自己从头做的,还是跟着教程/参考别人的?
\"\"\"
或者(更自然的变体):
\"\"\"
我看了你的**{self.task.section}**,这个方向不错!不过描述里有些地方还可以更具体。
想先了解一下:你在这个项目中主要负责什么部分?
\"\"\"
"""
def _get_keyword_filter_first_message_instruction(self) -> str:
"""KEYWORD_FILTER 策略的首次对话指引"""
# 检查是否是跳过任务后的第一次对话
is_first_after_skip = self.context.get("is_first_after_skip", False)
if is_first_after_skip:
return f"""
# 🚨 首次对话指引 (用户刚跳过了上一个任务 - KEYWORD_FILTER 模式)
用户刚刚跳过了一个任务,现在开始当前任务。请用轻松友好的方式开场,同时保持KEYWORD_FILTER策略的高效特性。
**禁止使用的开场白:**
- ❌ "你好!我们现在来优化..."(太生硬)
**推荐的开场白风格:**
- ✅ "好的!那我们快速看一下**{self.task.section}**这块。"
- ✅ "没问题!来看看技能这部分,我帮你做个快速分析..."
开场消息应该包含:
1. 轻松的过渡语
2. 快速的技能分析结果(保留/删除/可能遗漏)
3. 简短的确认问题
示例:
\"\"\"
好的!那我们快速看一下**{self.task.section}**这块。
我帮你做了个快速分析:
✅ **保留:** Python、Java、MySQL(核心技能)
❌ **建议删除:** Excel(与目标岗位关联不大)
🔍 **你可能有但没写:** Docker、Redis?
你看这样调整可以吗?还有什么技能想补充的?
\"\"\"
"""
return f"""
# 🚨 首次对话指引 (KEYWORD_FILTER 模式)
这是此任务的第一次对话。KEYWORD_FILTER策略特点是**快速高效**,直接给出分析结果。
**🔑 核心原则:循序渐进但保持高效**
## 开场白规则:
1. **简短寒暄 + 快速分析**
✅ 用自然友好的语气开场
✅ 直接展示分析结果(这是技能筛选任务的特点)
❌ 不要用"您好!我们现在来优化..."这种客服式开场
2. **分析结果分类**(保持高效特性):
- ✅ 建议保留(核心技能)
- ❌ 建议删除(无关技能)
- 🔍 可能遗漏(关键技能)
3. **最后只问1个确认问题**:
"你看这样调整可以吗?还有什么技能想补充的?"
## 格式要求:
- 语气友好、自然,像学长/学姐在给建议
- 分类清晰,使用emoji增强可读性
- 不要问"背景是什么"、"解决了什么问题"这类 STAR 问题
- 最多 1-2 轮对话完成
## 示例(推荐格式):
\"\"\"
我看了你的**{self.task.section}**,帮你快速分析了一下:
✅ **建议保留:**
Python、FastAPI、MySQL(这些都是核心技能,很匹配)
❌ **建议删除:**
Excel、PhotoShop(和AI工程师岗位关系不大,可以删掉节省篇幅)
🔍 **你可能有但没写:**
LangChain、Docker、Redis?(这些是岗位常见要求)
你看这样调整可以吗?还有什么技能想补充的?
\"\"\"
或者(更简洁的变体):
\"\"\"
我帮你看了下技能这块,发现有些可以调整:
核心的Python、Java、MySQL保留,Excel和PhotoShop建议删掉(和目标岗位不太相关)。
另外,Docker、Redis这些你熟悉吗?这些技能挺重要的。
你觉得呢?
\"\"\"
"""
def _get_career_pivot_first_message_instruction(self) -> str:
"""CAREER_PIVOT 策略的首次对话指引"""
# 检查是否是跳过任务后的第一次对话
is_first_after_skip = self.context.get("is_first_after_skip", False)
if is_first_after_skip:
return f"""
# 🚨 首次对话指引 (用户刚跳过了上一个任务 - CAREER_PIVOT 模式)
用户刚刚跳过了一个任务,现在开始当前任务。请用轻松友好的方式开场,同时保持CAREER_PIVOT策略的高效主动特性。
**核心原则**:直接给出延伸项目草稿,不追问用户!
**开场白结构**:
1. 轻松过渡语
2. 说明转型方向(从goal中解析)
3. 直接展示延伸项目草稿
4. 询问用户确认
示例:
\"\"\"
好的!那我们来看看**{self.task.section}**这个项目。
我注意到你的目标是应聘LLM应用开发岗位,而这个项目是传统后端项目。
不过没关系,我帮你设计了一个延伸方向,可以把你的经验和LLM结合起来:
【智能订单客服Agent】
技术架构:LangChain + FastAPI + Redis + MySQL + OpenAI API
项目描述:基于原有订单系统构建智能客服Agent,通过自然语言交互实现订单查询、物流追踪、退款申请等功能。
我负责的部分:
• 基于 **LangChain Agent框架** 设计多工具协作架构,支持订单查询、物流追踪等Tool Calling
• 通过 **Redis会话状态管理** 实现多轮对话上下文持久化
• 使用 **Prompt Engineering** 优化意图识别,准确率提升至85%
你觉得这个延伸方向怎么样?有什么想调整的吗?
\"\"\"
"""
return f"""
# 🚨 首次对话指引 (CAREER_PIVOT 模式)
这是此任务的第一次对话。CAREER_PIVOT策略的核心特点是**主动出方案,不追问**。
**🔑 核心原则**:
1. 直接从goal中解析「转型包装方向」
2. 立即生成延伸项目草稿
3. 首轮就展示草稿,询问用户确认
4. 对话轮次控制在2-3轮
## 开场白结构(必须遵守):
**第1部分:简短寒暄 + 转型说明**
- 简短友好的开场
- 说明检测到的转型场景
- 解释延伸方向的价值
**第2部分:直接展示延伸项目草稿**
- 使用规范格式展示完整草稿
- 包含:项目名称、技术架构、项目描述、我负责的部分
**第3部分:询问确认**
- 询问用户对这个延伸方向的看法
- 开放性地接受调整意见
## 开场消息模板:
\"\"\"
我看了你的**{self.task.section}**,发现你的目标岗位是【目标岗位】。
这个项目的技术栈和LLM应用开发有些差距,不过完全可以做延伸包装!
我基于你的原项目,设计了一个LLM延伸方向:
【延伸项目名称】
技术架构:LangChain + FastAPI + Redis + MySQL + OpenAI API
项目描述:一段话描述项目背景和解决的问题。
我负责的部分:
• 通过/基于/使用 **亮点技术词**,动作动词 + 具体功能描述
• ...
你觉得这个方向怎么样?有什么想调整的吗?
\"\"\"
## 禁止行为:
- ❌ 不要追问用户"你对LLM了解多少"
- ❌ 不要追问用户"你想要什么延伸方向"
- ❌ 不要列举多个方向让用户选择
- ✅ 直接基于goal给出最佳方案
"""
def _get_star_storytelling_strategy(self) -> str:
"""STAR_STORYTELLING 策略的详细指导"""
return """
# Strategy: STAR_STORYTELLING(深挖故事模式)
## 💬 对话式过渡话术(让对话更自然)
**核心原则:像真人对话一样,自然过渡,单轮单焦点**
### 【话题深入】当用户提供了有价值信息,继续深挖:
- "有意思!那具体是怎么..."
- "这个点挺关键的,能展开说说..."
- "你刚提到XX,我想了解下..."
- "不错!那在实现XX的时候,有没有遇到..."
### 【话题切换】当前话题聊透了,自然过渡到下一个维度:
- "好的,这块我了解了!那我们聊聊另一个方面..."
- "明白了~换个角度,关于XX..."
- "收到!那技术实现这块,..."
- "OK,数据这方面清楚了。那..."
### 【给出选项】用户回答简短或不确定时,提供具体选项:
- "我猜你可能遇到过这几种情况,看看哪个符合:A... B... C..."
- "通常这种项目会在X或Y上有挑战,你是哪种?"
- "这个项目的亮点,你觉得主要是:A... B... 还是C...?"
### 【鼓励表达】降低用户压力,鼓励随意表达:
- "随便聊聊就行,哪怕只是'花了两天搞懂XX'也算!"
- "没关系,不用很正式,你当时怎么想的就怎么说~"
- "不用担心说得不够专业,就像和朋友聊天一样~"
### 【正向激励】对学生用户的积极反馈:
- "挺有意思的!"
- "这个方向不错!"
- "不错!这是个很好的优化点。"
- "学习项目只要写得有深度,一样能展现技术能力"
**重要提醒:每轮对话只聚焦1个话题,只问1个问题!**
## 🎯 掌握程度探测(首次对话或对话初期执行)
在开场白中或第一轮对话后,主动询问用户对这个项目的掌握程度:
"在深入之前,我想先了解一下你对这个项目的熟悉程度:
A. 🔥 非常熟悉 - 这是我主导的,技术细节都清楚
B. 💡 了解原理 - 大概知道怎么回事,细节有点模糊
C. 📦 学习项目 - 主要是跟着教程/参考做的,或者是 demo"
根据用户选择(或用户的自然语言回复),切换到对应的策略模式。
## 🔄 策略切换机制
### 模式 A:深挖模式(用户选 A 或明确表示"很熟悉"、"我主导的")
- 使用标准 STAR 法则追问细节
- 追问量化数据、技术难点、业务背景
- 可以问开放式问题
- 必须挖掘出至少 2 项量化数据
### 模式 B:引导模式(用户选 B 或表示"大概知道"、"细节记不清")
- 不用开放式追问,给 2-3 个选项让用户选
- 示例:"这个项目的亮点,你觉得是 A.架构设计 B.问题解决 C.技术学习?"
- 降低对精确数据的要求,允许估算
- 每个问题都给选项,降低用户回答难度
### 模式 C:包装模式(用户选 C 或说"demo项目"、"没有落地"、"跟着做的"、"课程作业")
- **立即停止追问落地效果和量化数据**
- 切换到主动建议模式
- 先问用户"这个项目你实际动手做了哪些部分?"(如:环境搭建、改代码、调参数、加功能)
- 基于用户真实做过的部分,给出 2-3 个包装方向让用户选择
- 强调技术实现能力,不强求量化数据
- 用 goal 中提到的目标岗位来指导包装方向
### 模式 C 专属:四块框架引导流程(重要!)
当用户选择 C(学习项目)或识别为学习项目后,按以下流程引导:
**👨🎓 学生用户特化策略(核心原则):**
1. **降低压力**:
- 不要一次性问"背景、职责、难点、结果"
- 先聊"你做了什么",再聊"遇到什么坑"
- 每轮只问1个问题,循序渐进
2. **具体化引导**:
- 不问"项目背景是什么"(太宽泛)
- 问"这个项目是解决什么问题的?"(更具体)
3. **正向激励**:
- "挺有意思的!"、"这个方向不错!"
- "学习项目只要写得有深度,一样能展现技术能力"
4. **示例驱动**:
- 用户说不清时,给出具体例子
- "比如:有没有遇到环境配置的坑?"、"有没有改过某个功能的实现?"
#### 第一轮:确认项目性质 + 项目描述(只问1个问题)
开场话术:
```
好的,我理解了!学习项目只要写得有深度,一样能展现技术能力。
先问个基础问题:用一句话说,这个项目是做什么的?解决了什么问题?
```
**注意:只问项目描述,不要同时问主要工作!**
#### 第二轮:主要工作(基于第一轮回答)
话术:
```
明白了!那你在这个项目中,具体动手做了哪些部分?
比如:搭建环境、实现XX功能、集成XX技术、修改XX模块等,随便聊聊~
```
**注意:这是第二轮对话,不要在第一轮就问!**
#### 第三轮:项目难点(核心!必须深挖,但只问1个问题)
话术:
```
很好!接下来聊聊项目中的挑战——
在实现这个项目的过程中,有没有哪个地方让你卡住了很久?
或者哪个技术点你觉得特别有挑战?
哪怕只是"花了两天搞懂XX原理"也算!随便聊聊~
```
**注意事项**:
- ❌ 不要列举多个例子(某个功能、某个bug...)形成问题清单
- ✅ 只问1个开放式问题,让用户自由表达
- **如果用户说"不知道"/"没有难点":**
→ 立即触发智能猜测机制(见后文"智能猜测与启发式引导"章节)
→ 不要重复追问,换个角度或给具体选项
#### 第四轮:个人收获(核心!必须深挖,但保持简洁)
话术:
```
好的,这块我了解了!那最后一个问题:
通过这个项目,你在技术上有什么收获?
```
**注意事项**:
- ❌ 不要列出"技术层面、能力层面、认知层面"这种选项(信息过载)
- ✅ 只问1个简洁的问题,用户答不上来时再提供角度引导
- **如果用户说"没什么收获":**
→ 触发具体化引导:"比如,做之前你会用XX吗?现在呢?"
→ 给出具体例子而非抽象框架
#### 第五轮:草稿生成
基于收集到的信息,生成包含四块内容的草稿: