generated from kgod/ai-review-template
feat: initialize resume agent with OfferPai sync
This commit is contained in:
@@ -0,0 +1,550 @@
|
||||
"""Grounded experience optimization backed by structured model output."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Protocol
|
||||
|
||||
from .experience_optimizer_models import ExperienceOptimizationOutput
|
||||
from .llm_services import LLMServiceError, OpenAICompatibleStructuredClient, log_ai_event
|
||||
from .settings import Settings
|
||||
|
||||
Fact = dict[str, str]
|
||||
|
||||
|
||||
class ExperienceOptimizer(Protocol):
|
||||
def optimize(
|
||||
self, entry: dict[str, Any], *, context: dict[str, Any], facts: list[Any]
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
|
||||
class OpenAIExperienceOptimizer:
|
||||
"""Build a grounded proposal from user facts; RAG is style-only context."""
|
||||
|
||||
def __init__(self, completion: Any, retriever: Any = None, embedder: Any = None) -> None:
|
||||
self.completion = completion
|
||||
self.retriever = retriever
|
||||
self.embedder = embedder
|
||||
|
||||
def optimize(
|
||||
self, entry: dict[str, Any], *, context: dict[str, Any], facts: list[Any]
|
||||
) -> dict[str, Any]:
|
||||
ledger = normalize_fact_ledger(facts)
|
||||
output: ExperienceOptimizationOutput = self.completion.complete(
|
||||
schema=ExperienceOptimizationOutput,
|
||||
schema_name="experience_optimization",
|
||||
system_prompt=_OPTIMIZATION_PROMPT,
|
||||
payload={
|
||||
"user_fact_ledger": ledger,
|
||||
"primary_narrative": str(entry.get("description") or "").strip(),
|
||||
"resume_context": {
|
||||
"target_position": context.get("target_position"),
|
||||
"major": context.get("major"),
|
||||
"entry_type": context.get("entry_type"),
|
||||
"optimization_mode": context.get("optimization_mode", "light"),
|
||||
"user_instruction": context.get("instruction"),
|
||||
},
|
||||
"deep_interview": {
|
||||
"completion": context.get("interview_completion") or {},
|
||||
"completed_dimensions": context.get("completed_dimensions") or [],
|
||||
"question_history": context.get("question_history") or [],
|
||||
},
|
||||
"style_references": self._retrieve(ledger, context),
|
||||
},
|
||||
)
|
||||
proposal = self._with_fact_coverage(
|
||||
self._grounded_proposal(output.model_dump(), ledger), ledger
|
||||
)
|
||||
reason = self._quality_reason(proposal, entry, ledger)
|
||||
if reason:
|
||||
proposal = self._repair(entry, context, ledger, proposal, reason)
|
||||
proposal["source"] = "ai_expanded"
|
||||
return proposal
|
||||
|
||||
def _repair(
|
||||
self,
|
||||
entry: dict[str, Any],
|
||||
context: dict[str, Any],
|
||||
ledger: list[Fact],
|
||||
proposal: dict[str, Any],
|
||||
reason: str,
|
||||
) -> dict[str, Any]:
|
||||
log_ai_event(
|
||||
"experience_optimization_repair_started",
|
||||
reason_code=reason,
|
||||
entry_type=str(context.get("entry_type") or ""),
|
||||
optimization_mode=str(context.get("optimization_mode") or "light"),
|
||||
required_fact_count=len(self._required_fact_ids(ledger)),
|
||||
omitted_fact_count=len(proposal.get("omitted_fact_ids") or []),
|
||||
)
|
||||
try:
|
||||
repaired: ExperienceOptimizationOutput = self.completion.complete(
|
||||
schema=ExperienceOptimizationOutput,
|
||||
schema_name="experience_optimization_repair",
|
||||
system_prompt=_OPTIMIZATION_REPAIR_PROMPT,
|
||||
payload={
|
||||
"user_fact_ledger": ledger,
|
||||
"primary_narrative": str(entry.get("description") or "").strip(),
|
||||
"canonical_fact_draft": _canonical_fact_draft(ledger),
|
||||
"rejected_candidate": proposal,
|
||||
"rejected_reason": reason,
|
||||
"required_fact_ids": self._required_fact_ids(ledger),
|
||||
"omitted_fact_ids": proposal.get("omitted_fact_ids") or [],
|
||||
"entry_type": context.get("entry_type"),
|
||||
"resume_context": {
|
||||
"target_position": context.get("target_position"),
|
||||
"major": context.get("major"),
|
||||
"optimization_mode": context.get("optimization_mode", "light"),
|
||||
},
|
||||
"deep_interview": {
|
||||
"completion": context.get("interview_completion") or {},
|
||||
"completed_dimensions": context.get("completed_dimensions") or [],
|
||||
"question_history": context.get("question_history") or [],
|
||||
},
|
||||
"validation_requirements": [
|
||||
"optimized_description must not be empty",
|
||||
"every claim must cite only user_fact_ledger IDs",
|
||||
"retain every material confirmed fact; only merge genuinely duplicate wording",
|
||||
"put non-blocking improvement ideas in optional_enhancements",
|
||||
"do not put unconfirmed identity facts into optimized_description",
|
||||
"do not return punctuation-only text or a raw field dump",
|
||||
],
|
||||
},
|
||||
)
|
||||
except LLMServiceError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise LLMServiceError(
|
||||
"Experience optimization repair failed",
|
||||
reason_code="repair_failed",
|
||||
stage="experience_repair",
|
||||
safe_summary=type(exc).__name__,
|
||||
) from exc
|
||||
|
||||
repaired_proposal = self._with_fact_coverage(
|
||||
self._grounded_proposal(repaired.model_dump(), ledger), ledger
|
||||
)
|
||||
repaired_reason = self._quality_reason(repaired_proposal, entry, ledger)
|
||||
if repaired_reason == "empty_result":
|
||||
log_ai_event(
|
||||
"experience_optimization_repair_rejected",
|
||||
reason_code=repaired_reason,
|
||||
entry_type=str(context.get("entry_type") or ""),
|
||||
optimization_mode=str(context.get("optimization_mode") or "light"),
|
||||
)
|
||||
raise LLMServiceError(
|
||||
"Repaired model output was empty",
|
||||
reason_code="empty_result",
|
||||
stage="experience_repair_validation",
|
||||
safe_summary=repaired_reason,
|
||||
)
|
||||
if repaired_reason:
|
||||
repaired_proposal.setdefault("validation_warnings", []).append(
|
||||
"material_fact_omitted_after_repair"
|
||||
if repaired_reason == "material_fact_omitted"
|
||||
else repaired_reason
|
||||
)
|
||||
log_ai_event(
|
||||
"experience_optimization_repair_relaxed",
|
||||
reason_code=repaired_reason,
|
||||
entry_type=str(context.get("entry_type") or ""),
|
||||
optimization_mode=str(context.get("optimization_mode") or "light"),
|
||||
)
|
||||
return repaired_proposal
|
||||
|
||||
@staticmethod
|
||||
def _grounded_proposal(output: dict[str, Any], ledger: list[Fact]) -> dict[str, Any]:
|
||||
from .claim_validator import validate_proposal
|
||||
|
||||
return validate_proposal(output, ledger)
|
||||
|
||||
@staticmethod
|
||||
def _quality_reason(
|
||||
proposal: dict[str, Any], entry: dict[str, Any], ledger: list[Fact]
|
||||
) -> str | None:
|
||||
# A candidate that drops confirmed material facts (feature lists, product
|
||||
# intro, outcomes) gets exactly one repair pass. If the repair still omits
|
||||
# them, _repair relaxes with a warning — omissions never veto the draft.
|
||||
optimized = str(proposal.get("optimized_description") or "").strip()
|
||||
if not optimized or not any(character.isalnum() for character in optimized):
|
||||
return "empty_result"
|
||||
if proposal.get("omitted_fact_ids"):
|
||||
return "material_fact_omitted"
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _required_fact_ids(ledger: list[Fact]) -> list[str]:
|
||||
return required_material_fact_ids(ledger)
|
||||
|
||||
@classmethod
|
||||
def _with_fact_coverage(cls, proposal: dict[str, Any], ledger: list[Fact]) -> dict[str, Any]:
|
||||
required = cls._required_fact_ids(ledger)
|
||||
narrative = " ".join(
|
||||
[
|
||||
str(proposal.get("optimized_description") or ""),
|
||||
*[str(item) for item in proposal.get("bullets") or []],
|
||||
]
|
||||
)
|
||||
covered = [
|
||||
fact_id
|
||||
for fact_id in required
|
||||
if _fact_text_is_preserved(fact_id, ledger, narrative)
|
||||
]
|
||||
proposal["covered_fact_ids"] = covered
|
||||
proposal["omitted_fact_ids"] = [
|
||||
fact_id for fact_id in required if fact_id not in covered
|
||||
]
|
||||
return proposal
|
||||
|
||||
def style_references(self, facts: list[Any], context: dict[str, Any]) -> list[Any]:
|
||||
"""Expose non-blocking RAG style examples to the gap-analysis role."""
|
||||
try:
|
||||
return self._retrieve(normalize_fact_ledger(facts), context)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _retrieve(
|
||||
self, ledger: list[Fact], context: dict[str, Any]
|
||||
) -> list[dict[str, str]]:
|
||||
query = " ".join(item["text"] for item in ledger)
|
||||
if context.get("target_position"):
|
||||
query = f"{context['target_position']} {query}".strip()
|
||||
try:
|
||||
results = self.retriever.retrieve(
|
||||
query_text=query or "resume experience optimization",
|
||||
embedder=self.embedder,
|
||||
position_category=context.get("target_position") or None,
|
||||
exp_type=context.get("entry_type") or None,
|
||||
k=3,
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
return [
|
||||
{
|
||||
"id": str(item.get("id") or f"rag_{index}"),
|
||||
"title": str(item.get("title") or item.get("title_path") or "reference"),
|
||||
"content": str(item.get("content") or item.get("optimized") or ""),
|
||||
"writing_points": str(item.get("points") or ""),
|
||||
}
|
||||
for index, item in enumerate(results[:3], start=1)
|
||||
]
|
||||
|
||||
|
||||
class FallbackExperienceOptimizer:
|
||||
def __init__(self, primary: ExperienceOptimizer, fallback: ExperienceOptimizer) -> None:
|
||||
self.primary = primary
|
||||
self.fallback = fallback
|
||||
|
||||
def optimize(
|
||||
self, entry: dict[str, Any], *, context: dict[str, Any], facts: list[Any]
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
proposal = self.primary.optimize(entry, context=context, facts=facts)
|
||||
proposal["generation_source"] = "llm"
|
||||
return proposal
|
||||
except Exception as exc:
|
||||
reason = _fallback_reason(exc)
|
||||
log_ai_event(
|
||||
"experience_optimization_failed",
|
||||
reason_code=reason,
|
||||
trace_id=getattr(exc, "trace_id", None),
|
||||
stage=getattr(exc, "stage", "experience_optimization"),
|
||||
entry_type=str(context.get("entry_type") or ""),
|
||||
optimization_mode=str(context.get("optimization_mode") or "light"),
|
||||
exception=type(exc).__name__,
|
||||
)
|
||||
if isinstance(exc, LLMServiceError):
|
||||
raise
|
||||
proposal = self.fallback.optimize(entry, context=context, facts=facts)
|
||||
proposal["generation_source"] = "rule_fallback"
|
||||
proposal["fallback_reason"] = reason
|
||||
return proposal
|
||||
|
||||
def style_references(self, facts: list[Any], context: dict[str, Any]) -> list[Any]:
|
||||
provider = getattr(self.primary, "style_references", None)
|
||||
if provider is None:
|
||||
return []
|
||||
try:
|
||||
return list(provider(facts, context) or [])
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
class RuleStructuredExperienceOptimizer:
|
||||
"""Offline generator for tests and explicit no-model fallback mode."""
|
||||
|
||||
def optimize(
|
||||
self, entry: dict[str, Any], *, context: dict[str, Any], facts: list[Any]
|
||||
) -> dict[str, Any]:
|
||||
ledger = normalize_fact_ledger(facts)
|
||||
description = str(entry.get("description") or "").strip()
|
||||
material = "; ".join(dict.fromkeys(item["text"] for item in ledger))
|
||||
if not material:
|
||||
return {
|
||||
"optimized_description": "",
|
||||
"changes": [],
|
||||
"missing_facts": ["specific action", "method or tool", "verifiable result"],
|
||||
"star": {},
|
||||
"claims": [],
|
||||
"bullets": [],
|
||||
"source": "rule_structured",
|
||||
"generation_source": "rule",
|
||||
"fallback_reason": "insufficient_user_facts",
|
||||
}
|
||||
optimized = material
|
||||
return {
|
||||
"optimized_description": optimized,
|
||||
"bullets": [optimized],
|
||||
"changes": ["reorganized confirmed actions and facts"],
|
||||
"missing_facts": _missing_facts(material),
|
||||
"star": {
|
||||
"situation": description or None,
|
||||
"task": str(entry.get("title") or entry.get("position") or "") or None,
|
||||
"action": material,
|
||||
"result": _known_result(material),
|
||||
},
|
||||
"claims": [],
|
||||
"source": "rule_structured",
|
||||
"generation_source": "rule",
|
||||
}
|
||||
|
||||
|
||||
def _fallback_reason(exc: Exception) -> str:
|
||||
if isinstance(exc, LLMServiceError):
|
||||
return exc.reason_code
|
||||
return type(exc).__name__.lower()[:48]
|
||||
|
||||
|
||||
def build_experience_optimizer(
|
||||
settings: Settings, client: Any | None = None
|
||||
) -> ExperienceOptimizer:
|
||||
"""Light optimizer: pure LLM on user facts (RAG knowledge base removed)."""
|
||||
rules = RuleStructuredExperienceOptimizer()
|
||||
if not settings.use_openai:
|
||||
return rules
|
||||
completion = OpenAICompatibleStructuredClient(settings, client)
|
||||
primary: ExperienceOptimizer = OpenAIExperienceOptimizer(completion)
|
||||
return FallbackExperienceOptimizer(primary, rules) if settings.fallback_to_rules else primary
|
||||
|
||||
|
||||
def normalize_fact_ledger(facts: list[Any]) -> list[Fact]:
|
||||
ledger: list[Fact] = []
|
||||
for index, value in enumerate(facts, start=1):
|
||||
if isinstance(value, dict):
|
||||
text = str(value.get("text") or "").strip()
|
||||
fact = {
|
||||
"id": str(value.get("id") or f"fact_{index}"),
|
||||
"source": str(value.get("source") or "user_form"),
|
||||
"field": str(value.get("field") or "unknown"),
|
||||
"text": text,
|
||||
}
|
||||
else:
|
||||
text = str(value).strip()
|
||||
fact = {
|
||||
"id": f"fact_{index}",
|
||||
"source": "user_form",
|
||||
"field": "unknown",
|
||||
"text": text,
|
||||
}
|
||||
if text:
|
||||
ledger.append(fact)
|
||||
return _append_description_parts(ledger)
|
||||
|
||||
|
||||
_DESCRIPTION_ITEM_MARKER = re.compile(r"^\s*\d+\s*[.、))]\s*")
|
||||
_DESCRIPTION_LINE_LABEL = re.compile(r"^[\u4e00-\u9fff]{2,8}[::]\s*")
|
||||
|
||||
|
||||
def split_description_parts(text: str) -> list[str]:
|
||||
"""Split a structured description into independently checkable fragments.
|
||||
|
||||
A long multi-line description judged as one fact lets dropped features hide
|
||||
behind the overall n-gram coverage of the kept tech stack. Line/clause
|
||||
fragments make each feature, intro, or outcome its own gate entry. Short
|
||||
single-sentence descriptions stay unsplit (one fragment -> caller keeps the
|
||||
parent fact).
|
||||
"""
|
||||
parts: list[str] = []
|
||||
for raw_line in str(text).splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
segments = re.split(r"[。;;]", line) if len(line) > 40 else [line]
|
||||
for segment in segments:
|
||||
part = _DESCRIPTION_LINE_LABEL.sub("", _DESCRIPTION_ITEM_MARKER.sub("", segment.strip())).strip()
|
||||
if len(part) >= 4:
|
||||
parts.append(part)
|
||||
return parts
|
||||
|
||||
|
||||
def _append_description_parts(ledger: list[Fact]) -> list[Fact]:
|
||||
expanded: list[Fact] = []
|
||||
for fact in ledger:
|
||||
expanded.append(fact)
|
||||
if fact.get("field") != "description":
|
||||
continue
|
||||
parts = split_description_parts(fact["text"])
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
for part_index, part in enumerate(parts, start=1):
|
||||
expanded.append(
|
||||
{
|
||||
"id": f"{fact['id']}_part_{part_index}",
|
||||
"source": fact.get("source") or "user_form",
|
||||
"field": "description_part",
|
||||
"text": part,
|
||||
}
|
||||
)
|
||||
return expanded
|
||||
|
||||
|
||||
def required_material_fact_ids(ledger: list[Fact]) -> list[str]:
|
||||
"""Ids of facts that must survive in the narrative.
|
||||
|
||||
When a description was split into fragments, the fragments stand in for the
|
||||
parent so coverage is judged per fragment, not per whole entry.
|
||||
"""
|
||||
split_parents = {
|
||||
fact["id"].rsplit("_part_", 1)[0]
|
||||
for fact in ledger
|
||||
if fact.get("field") == "description_part"
|
||||
}
|
||||
return [
|
||||
fact["id"]
|
||||
for fact in ledger
|
||||
if _is_material_resume_fact(fact) and fact["id"] not in split_parents
|
||||
]
|
||||
|
||||
|
||||
def _is_material_resume_fact(fact: Fact) -> bool:
|
||||
"""Facts that belong in the narrative rather than only in card metadata."""
|
||||
if fact.get("field") in {
|
||||
"degree", "start_date", "end_date_or_present", "date", "title", "name",
|
||||
"company", "organization", "school", "project_name", "position", "role",
|
||||
"project_role", "major", "award",
|
||||
}:
|
||||
return False
|
||||
return bool(str(fact.get("text") or "").strip()) and (
|
||||
fact.get("field") in {"description", "description_part"}
|
||||
or fact.get("source") == "user_answer"
|
||||
)
|
||||
|
||||
|
||||
|
||||
def _fact_text_is_preserved(fact_id: str, ledger: list[Fact], narrative: str) -> bool:
|
||||
fact = next((item for item in ledger if item["id"] == fact_id), None)
|
||||
if fact is None:
|
||||
return False
|
||||
source = _normalized_text(fact["text"])
|
||||
target = _normalized_text(narrative)
|
||||
if not source or not target:
|
||||
return False
|
||||
if source in target:
|
||||
return True
|
||||
|
||||
latin_terms = [
|
||||
_latin_stem(term)
|
||||
for term in re.findall(r"[A-Za-z][A-Za-z0-9+#._-]{1,}", fact["text"])
|
||||
if term.casefold() not in _LATIN_STOPWORDS
|
||||
]
|
||||
target_latin_terms = {
|
||||
_latin_stem(term)
|
||||
for term in re.findall(r"[A-Za-z][A-Za-z0-9+#._-]{1,}", narrative)
|
||||
if term.casefold() not in _LATIN_STOPWORDS
|
||||
}
|
||||
latin_covered = not latin_terms or sum(
|
||||
term in target_latin_terms for term in latin_terms
|
||||
) >= max(1, int(len(latin_terms) * 0.6 + 0.999))
|
||||
if not latin_covered:
|
||||
return False
|
||||
|
||||
chinese_segments = re.findall(r"[\u4e00-\u9fff]{2,}", fact["text"])
|
||||
chinese_ngrams = {
|
||||
segment[index:index + size]
|
||||
for segment in chinese_segments
|
||||
for size in (2, 3, 4)
|
||||
for index in range(max(0, len(segment) - size + 1))
|
||||
}
|
||||
matched_ngrams = sum(
|
||||
_normalized_text(token) in target for token in chinese_ngrams
|
||||
)
|
||||
chinese_covered = not chinese_ngrams or matched_ngrams >= max(
|
||||
1, int(len(chinese_ngrams) * 0.65)
|
||||
)
|
||||
number_tokens = re.findall(r"\d+(?:\.\d+)?%?", fact["text"])
|
||||
numbers_covered = all(token in narrative for token in number_tokens)
|
||||
return chinese_covered and numbers_covered and bool(
|
||||
latin_terms or chinese_ngrams or number_tokens
|
||||
)
|
||||
|
||||
|
||||
_LATIN_STOPWORDS = frozenset({
|
||||
"and", "are", "for", "from", "into", "its", "that", "the", "this", "through", "using", "with",
|
||||
})
|
||||
|
||||
|
||||
def _latin_stem(term: str) -> str:
|
||||
normalized = term.casefold().rstrip(".,;:!?")
|
||||
if normalized == "built":
|
||||
return "build"
|
||||
if normalized == "ran":
|
||||
return "run"
|
||||
for suffix in ("ing", "ed", "es", "s"):
|
||||
if normalized.endswith(suffix) and len(normalized) - len(suffix) >= 4:
|
||||
return normalized[:-len(suffix)]
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalized_text(text: str) -> str:
|
||||
return "".join(character.casefold() for character in text if character.isalnum())
|
||||
|
||||
|
||||
def _missing_facts(text: str) -> list[str]:
|
||||
missing: list[str] = []
|
||||
if not any(token in text for token in ("%", "result", "impact", "users")):
|
||||
missing.append("verifiable result or impact")
|
||||
if not any(token in text.casefold() for token in ("using", "with", "through", "via")):
|
||||
missing.append("method, tool, or collaboration approach")
|
||||
return missing or ["scope of responsibility"]
|
||||
|
||||
|
||||
def _known_result(text: str) -> str | None:
|
||||
markers = ("%", "improved", "reduced", "completed", "launched", "users")
|
||||
return text if any(marker in text.casefold() for marker in markers) else None
|
||||
|
||||
|
||||
def _canonical_fact_draft(ledger: list[Fact]) -> str:
|
||||
return "; ".join(dict.fromkeys(item["text"] for item in ledger if item.get("text")))
|
||||
|
||||
|
||||
_OPTIMIZATION_PROMPT = """
|
||||
你是一名中文简历经历编辑。只返回符合 output_json_schema 的 JSON。
|
||||
请将用户提供的经历改写为专业、可直接用于简历的中文正文,并采用自然的 STAR
|
||||
结构。完整性优先于篇幅:保留用户已确认的职责、动作、方法、工具、协作、范围、
|
||||
交付物和结果。可以重排和合并真正重复的措辞,但不得为了缩短文本删除有意义的事实;
|
||||
必要时可使用多句或多条要点。项目或产品的功能模块、平台定位/简介与量化成果,
|
||||
与技术栈同等重要:不得只保留技术栈而省略功能点、平台简介或成果描述。
|
||||
|
||||
deep_interview.completion.is_sufficient 为 true 时,表示 LangGraph 已确认当前候选稿
|
||||
所需信息足够。此时不得把任何已回答维度重新列为 missing_facts,也不要把泛泛的
|
||||
“补充技术栈、量化结果或职责”当作当前候选稿的阻塞条件。若存在不影响当前候选稿的
|
||||
提升方向,只能放入 optional_enhancements,且要明确是可选增强。
|
||||
|
||||
style_references 只用于学习表达方式,不是用户个人事实。不得虚构公司、学校、
|
||||
奖项、证书、日期或归属。可以基于用户的经历语义做自然的职业化改写、结构化归纳和适度的
|
||||
岗位导向扩展;不要因为原文没有逐字写出某个方法或影响就机械省略整段内容。量化表达
|
||||
应优先使用用户确认的数据;未确认时可以使用不带精确数字的合理影响描述。每条 claim
|
||||
必须只引用 user_fact_ledger 中的 evidence_ids,绝不能引用 rag_ IDs。
|
||||
""".strip()
|
||||
|
||||
_OPTIMIZATION_REPAIR_PROMPT = """
|
||||
你负责修复一份未通过确定性校验的中文简历候选稿。只返回符合 output_json_schema 的
|
||||
JSON。rejected_candidate 和 rejected_reason 只说明缺陷,不是新的事实来源。
|
||||
输出非空、专业、可直接用于简历的中文叙述,采用自然的 STAR 结构。修复的首要目标是保留 required_fact_ids
|
||||
对应的全部重要事实,包括原描述和深度追问答案中的动作、方法、工具、范围、协作、
|
||||
交付物和结果。不要为了简洁而压缩掉这些信息;可使用多句或多条要点,只合并语义重复
|
||||
的表达。允许自然的同义改写、结构化归纳和岗位导向扩展,不要求逐字复述每项事实。
|
||||
不得悄然编造公司、学校、奖项、证书、日期或归属。非阻塞的后续提升方向放入
|
||||
optional_enhancements。每条 claim 必须引用已有 user_fact_ledger evidence_ids。不要
|
||||
返回只有标点的文本或原始表单字段拼接。
|
||||
""".strip()
|
||||
Reference in New Issue
Block a user