
1. 项目背景某科技公司的 HR 部门每周收到约 200 份简历筛选初面的工作量大且主观性强。HR 经理想用 CrewAI 做一个简历筛选助手输入简历文本输出候选人的技能匹配度、风险点和面试建议然后用人系统自动归档。第一版实现中Agent 输出了这样一段文本“候选人张三3年Python经验做过2个数据分析项目。建议进入初面。沟通能力看起来不错。”虽然内容看起来有道理但下游系统完全无法解析——没有结构化的分数匹配度到底是多少没有字段分隔哪部分是技能评估哪部分是风险更无法批量处理200 份简历需要 200 次人工从文本中提取关键字段。核心痛点大模型的输出本质是自然语言天然非结构化。当 CrewAI 的输出需要被下游系统前端、数据库、另一个 Agent消费时“看起来对远远不够——必须格式对、字段全、值合规”。CrewAI 提供了三种结构化输出方式Markdown人类阅读友好、JSON程序解析友好、Pydantic Model强类型校验友好。本章的目标是掌握expected_output与结构化输出的协同使用让 Agent 的输出从看起来像回事变成系统能直接消费。2. 项目设计小胖喝着奶茶“大师Agent 输出不就是一段文字吗为什么还要搞结构化我直接用正则提取不就行了”大师“那你试试用正则提取’匹配度大概 70%-80% 吧’这句话。Agent 可能写成’70-80%‘也可能写成’约七到八成’甚至’匹配度较高’。你的正则需要覆盖所有这些变体——这本身就是个 NLP 问题。”小白翻开文档“CrewAI 的 Task 有个expected_output参数。我之前以为它只是个’提示’现在看文档说它实际上会被用于解析和校验”大师“对。expected_output不止是给 Agent 看的目标描述它也是 CrewAI 输出解析的’模板’。当你在expected_output里写了表格结构Agent 会倾向于输出表格当你在expected_output里定义了字段Agent 会倾向于包含这些字段。如果再配合output_json或output_pydanticCrewAI 会在模型输出后进行格式校验和解析。”小胖“那 Markdown、JSON、Pydantic 这三种格式怎么选”大师“Markdown 适合人类阅读的最终产物——比如报告、邮件正文、攻略文档。JSON 适合给程序消费——比如 API 响应、数据库写入。Pydantic 适合给强类型系统消费——你需要保证字段类型、值范围和必填项的场景。”小白“Pydantic 的优势在哪JSON 本身也可以有 schema 约束啊。”大师“Pydantic 的优势在于它不只是声明字段——它提供运行时校验。比如你声明score: int Field(ge0, le100)Pydantic 会在解析时校验 score 是不是在 0-100 之间的整数。如果 Agent 输出score: 八十五Pydantic 会抛出 ValidationError——这个异常可以被 Guardrail 捕获并触发重试。JSON 做不到这种级别的校验。”小胖“那如果 Agent 输出的 JSON 格式不对怎么办比如多了个逗号、少了引号”大师“这就是结构化输出的核心挑战——模型的输出不是 100% 合规的。CrewAI 的策略是用expected_output给模型一个’输出模板’降低格式错误概率用output_pydantic做’最终校验’解析失败时自动触发重试。但重试不是无限次的——max_retries参数控制。如果多次重试仍失败最终的异常需要业务层兜底处理。”小白“那输出契约应该怎么设计比如面向前端、后端、测试三个角色的输出。”大师“这是个好问题的进阶版本。向前端输出字段名用 camelCase 中文说明附带显示建议如’超过80分绿色显示’。向后端输出字段名用 snake_case 英文附带数据来源‘该值来自 Agent 模型推理’。向测试输出附带置信度字段让测试人员知道哪些字段需要重点人工核验。每个角色关心的维度不同输出契约也不同。”技术映射总结Markdown 用于给人看报告、摘要、文档JSON 用于给程序读API 响应、数据库写入Pydantic Model 用于给强类型系统用带校验、带默认值、带约束。expected_output是三者的共同前奏——它既指导 Agent 怎么写又为后续解析提供模板。3. 项目实战3.1 实战目标构建简历筛选 Crew输入简历文本输出 Pydantic 结构化的候选人评估结果——包含技能匹配度、风险点和面试建议。对比 Markdown 输出和 Pydantic 输出的下游消费效率。3.2 环境准备resume-screener-crew/ ├── .env ├── main.py ├── models.py # Pydantic 输出模型定义 ├── agents.py ├── tasks.py ├── crew.py ├── sample_resumes/ # 测试用简历样本 └── outputs/依赖crewai、pydantic、python-dotenv3.3 分步实现步骤1定义 Pydantic 输出模型目标用 Pydantic 定义结构化的候选人评估输出格式。# models.pyfrompydanticimportBaseModel,FieldfromtypingimportList,OptionalfromenumimportEnumclassRiskLevel(str,Enum):LOW低MEDIUM中HIGH高classInterviewDecision(str,Enum):RECOMMEND推荐面试CONSIDER可考虑NOT_RECOMMEND不推荐classSkillItem(BaseModel):单项技能评估name:strField(description技能名称)required:boolField(description是否为岗位必需技能)match_level:intField(ge0,le100,description匹配度评分 0-100100表示完全匹配)evidence:strField(description匹配证据简历中的具体描述或项目经验)classRiskItem(BaseModel):单项风险评估category:strField(description风险类别如稳定性、技能缺口、薪资预期)level:RiskLevelField(description风险等级)detail:strField(description风险描述)mitigation:Optional[str]Field(defaultNone,description缓解建议如有)classInterviewQuestion(BaseModel):面试建议问题question:strField(description建议在面试中提出的问题)purpose:strField(description提问目的如验证技术深度、评估沟通能力)classCandidateEvaluation(BaseModel):简历筛选评估结果——Pydantic 输出模型candidate_name:strField(description候选人姓名)overall_score:intField(ge0,le100,description综合评分 0-100)decision:InterviewDecisionField(description面试决策)skill_assessment:List[SkillItem]Field(description技能评估列表至少包含3项)highlights:List[str]Field(description候选人亮点2-3条)risks:List[RiskItem]Field(description风险项列表至少1项。如无明显风险则填写无显著风险)suggested_questions:List[InterviewQuestion]Field(description建议的面试问题2-3个)summary:strField(description一句话总结不超过50字)步骤2定义简历筛选 Agent目标创建专注于结构化评估的 Agent。# agents.pyfromcrewaiimportAgentdefcreate_resume_screener(llm):创建简历筛选 AgentreturnAgent(role资深技术招聘专家,goal(评估候选人简历与{job_title}岗位的匹配度输出结构化的评估结果技能匹配度、风险点、面试建议每个评估维度必须基于简历中的具体证据。),backstory(你在阿里巴巴和字节跳动担任过技术招聘负责人累计筛选过超过50000份简历。你的评估框架包括三个维度技能匹配度硬技能、经验相关性项目经验、风险信号频繁跳槽、技能断层、薪资倒挂。你坚持一个原则没有证据的判断就是偏见。每条评估都必须引用简历中的具体内容作为证据。),llmllm,verboseTrue,allow_delegationFalse,max_iter10)步骤3定义 Task含 Pydantic 输出目标创建使用 Pydantic 输出模型的 Task。# tasks.pyfromcrewaiimportTaskdefcreate_resume_screening_task(agent,resume_text:str,job_title:str):创建结构化简历筛选任务returnTask(descriptionf 请评估以下候选人对{job_title}岗位的匹配度。 【岗位要求】{job_title}岗位核心要求相关工作经验3年以上具备项目独立交付能力。 【候选人简历】{resume_text}【评估要求】 1. 逐项评估技能匹配度必须引用简历中的证据 2. 识别潜在风险跳槽频率、技能断档、项目经验夸大信号等 3. 生成针对性的面试问题 4. 输出决策建议 【输出约束】 - 所有评分必须是0-100的整数 - 风险等级只能是高/中/低 - 每项评估必须附带简历中的具体证据 ,expected_output(结构化的候选人评估结果包含\n- 综合评分(0-100)\n- 面试决策(推荐面试/可考虑/不推荐)\n- 至少3项技能评估(含匹配度评分和证据)\n- 亮点列表\n- 风险列表\n- 建议面试问题(2-3个)\n- 一句话总结),agentagent,output_pydanticNone,# 将由 crew.py 在运行时设置output_fileoutputs/evaluation.md# 额外保存可读版本)步骤4组装 Crew 并处理结构化输出目标组装 Crew获取 Pydantic 结构化的评估结果。# crew.pyfromcrewaiimportCrew,Process,LLMfromagentsimportcreate_resume_screenerfromtasksimportcreate_resume_screening_taskfrommodelsimportCandidateEvaluationimportosimportjsondefscreen_resume(resume_text:str,job_title:str)-CandidateEvaluation:筛选单份简历返回结构化评估结果llmLLM(modelos.getenv(MODEL_NAME,gpt-4o-mini),temperature0.2,# 筛选任务偏好确定性输出timeout120)agentcreate_resume_screener(llm)taskcreate_resume_screening_task(agent,resume_text,job_title)# 关键设置 Pydantic 输出模型task.output_pydanticCandidateEvaluation crewCrew(agents[agent],tasks[task],processProcess.sequential,verboseTrue)resultcrew.kickoff()# 获取 Pydantic 结构化结果ifresult.pydantic:evaluation:CandidateEvaluationresult.pydanticreturnevaluationelse:# 解析失败的兜底处理raiseValueError(f结构化输出解析失败。原始输出{result.raw[:500]})# main.pyfromdotenvimportload_dotenv load_dotenv()fromcrewimportscreen_resumefrommodelsimportCandidateEvaluation,InterviewDecisionimportjsonimportos# 模拟简历数据SAMPLE_RESUME 姓名张三 工作经历 - 2019-2022 某互联网公司 Python后端开发负责用户中心微服务架构设计 - 2022-至今 某AI初创公司 高级后端工程师主导LLM应用平台后端开发 技能Python(精通)、FastAPI(熟练)、PostgreSQL(熟练)、Docker(熟练)、 Redis(熟练)、K8s(了解)、Go(入门) 项目经验 - 主导设计日活百万的用户中心系统QPS峰值5000 - 基于LangChain搭建企业内部知识库问答系统 教育背景985高校 计算机科学 硕士 其他GitHub开源项目Star 200 defmain():os.makedirs(outputs,exist_okTrue)job_title高级后端工程师AI方向print(f正在评估候选人 - 岗位:{job_title}\n)try:evaluationscreen_resume(SAMPLE_RESUME,job_title)# 将Pydantic模型转为字典输出result_dictevaluation.model_dump()print(*60)print(【结构化评估结果】)print(*60)print(json.dumps(result_dict,ensure_asciiFalse,indent2))# 下游系统可直接使用print(f\n✅ 候选人{evaluation.candidate_name})print(f 综合评分{evaluation.overall_score}/100)print(f 面试决策{evaluation.decision.value})print(f 亮点{, .join(evaluation.highlights)})print(f⚠️ 风险项数{len(evaluation.risks)})# 保存结构化JSON供下游系统消费withopen(outputs/evaluation.json,w,encodingutf-8)asf:json.dump(result_dict,f,ensure_asciiFalse,indent2)print(f\n 结构化结果已保存至: outputs/evaluation.json)exceptValueErrorase:print(f❌ 评估失败{e})if__name____main__:main()3.4 运行结果python main.py典型输出正在评估候选人 - 岗位: 高级后端工程师AI方向 [Agent思考] 开始评估候选人张三... 【结构化评估结果】 { candidate_name: 张三, overall_score: 78, decision: 推荐面试, skill_assessment: [ { name: Python, required: true, match_level: 90, evidence: 2019年至今5年Python后端开发经验 }, { name: FastAPI, required: true, match_level: 85, evidence: 简历明确列出FastAPI(熟练) }, { name: AI/LLM相关经验, required: true, match_level: 70, evidence: 主导LLM应用平台后端开发基于LangChain搭建知识库系统 }, { name: K8s, required: false, match_level: 35, evidence: 简历标注K8s(了解)深度不足 } ], highlights: [ 日活百万级用户系统架构经验, 有LLM应用平台实战经验与岗位AI方向高度契合, GitHub开源项目有社区认可度 ], risks: [ { category: 稳定性, level: 中, detail: 当前公司任职2年上一份工作3年跳槽频率属正常范围, mitigation: null }, { category: 技能缺口, level: 中, detail: K8s和Go仅为入门水平若岗位需要云原生深度经验则存在缺口, mitigation: 面试中确认K8s的实际使用深度 } ], suggested_questions: [ { question: 你在LLM应用平台项目中具体负责了哪些后端模块遇到了什么技术挑战, purpose: 验证AI方向实际技术深度 }, { question: 用户中心日活百万时你们是如何做数据库扩展和高可用设计的, purpose: 验证架构能力和高并发经验 } ], summary: Python和FastAPI技能扎实AI方向有实战经验建议进入技术面进一步验证架构深度。 } ✅ 候选人张三 综合评分78/100 面试决策推荐面试 亮点日活百万级用户中心系统架构经验, 有LLM应用平台实战经验, GitHub开源项目有社区认可度 ⚠️ 风险项数2 结构化结果已保存至: outputs/evaluation.json3.5 输出解析失败的常见原因及解决失败模式症状原因解决JSON 格式错误解析抛 JSONDecodeError模型输出了非法 JSONPydantic 模式比 JSON 稍好——CrewAI 会尝试修复常见格式错误类型不匹配overall_score为字符串78分模型忽略了类型约束在 description 中强调评分必须是纯数字必填字段缺失risks列表为空但 Pydantic 要求至少1项模型判断无风险但未按要求输出在 expected_output 中明确如无风险输出’无显著风险’枚举值不匹配输出了建议面试而非推荐面试模型用了同义词在 description 中列出枚举的所有合法值3.6 测试验证# test_structured_output.pyimportpytestfrommodelsimport(CandidateEvaluation,SkillItem,RiskItem,RiskLevel,InterviewDecision,InterviewQuestion)frompydanticimportValidationErrordeftest_valid_evaluation():验证合法的评估对象可以成功创建eval_data{candidate_name:测试候选人,overall_score:75,decision:InterviewDecision.RECOMMEND,skill_assessment:[SkillItem(namePython,requiredTrue,match_level85,evidence5年经验)],highlights:[亮点1],risks:[RiskItem(category稳定性,levelRiskLevel.LOW,detail无明显风险)],suggested_questions:[InterviewQuestion(question测试问题,purpose测试目的)],summary:一句话总结}evaluationCandidateEvaluation(**eval_data)assertevaluation.overall_score75assertevaluation.decisionInterviewDecision.RECOMMENDdeftest_invalid_score_range():验证评分超出范围会抛出异常withpytest.raises(ValidationError):SkillItem(namePython,requiredTrue,match_level150,# 超出0-100evidence测试)deftest_invalid_risk_level():验证非法风险等级会抛出异常frompydanticimportValidationError# RiskLevel 是枚举非法值会抛错withpytest.raises(ValidationError):RiskItem(category测试,level超高,detail测试)deftest_enum_values():验证枚举值定义正确assertInterviewDecision.RECOMMEND.value推荐面试assertRiskLevel.HIGH.value高assertRiskLevel.LOW.value低deftest_model_can_serialize():验证模型可以正确序列化为JSONeval_dataCandidateEvaluation(candidate_name测试,overall_score80,decisionInterviewDecision.RECOMMEND,skill_assessment[SkillItem(namePython,requiredTrue,match_level90,evidence测试)],highlights[亮点],risks[RiskItem(category稳定性,levelRiskLevel.LOW,detail无风险)],suggested_questions[InterviewQuestion(questionQ1,purpose测试)],summary总结)json_streval_data.model_dump_json()assert测试injson_strassert80injson_strpytest test_structured_output.py-v# 5 passed4. 项目总结4.1 优点与缺点维度Pydantic 输出JSON 输出Markdown 输出类型安全高运行时校验中需手动校验无下游消费简单直接.model_dump()简单json.loads()需解析/正则提取字段约束强ge/le/enum/必填无无人类可读性低需工具展示中可读但不易读高天然可读解析失败率低CrewAI 会重试中不适用给模型自由度低强约束可能限制深度中高4.2 适用场景推荐 Pydantic 输出的场景需要入库的结构化数据用户画像、订单摘要、评估报告下游系统强依赖字段类型和值的范围计费、风控、合规批量处理的 AI 产出物200份简历、1000条用户反馈前后端分离场景下的 API 响应有自动化测试覆盖的输出校验不推荐使用的场景创意类任务写文案、写报告正文一次性人工消费的输出用 Markdown 更合适4.3 注意事项Pydantic Field 的 description 很重要它会被传递给 Agent 作为输出约束写成技能匹配度(0-100)“而不是分数”。枚举值要用中文如果业务语言是中文枚举值也用中文定义减少模型翻译时的偏差。不要过度嵌套Pydantic 模型嵌套超过 3 层时模型的输出准确率显著下降。兜底处理始终在result.pydantic为 None 时有降级策略如用 Markdown 输出 人工处理。4.4 常见踩坑经验案例1模型输出的 JSON 字段名是英文但 Pydantic 定义是中文现象Pydantic 解析报field not found。根因Agent 的 backstory 是英文但 Pydantic 字段是中文模型在切换语言时迷路。解决确保 Agent 的 role/goal/backstory 语言与 Pydantic Field description 语言一致。案例2必填列表为空导致解析失败现象risks字段定义为List[RiskItem]但 Agent 输出[]Pydantic 校验通过但业务不合规。根因Pydantic 的List默认允许空列表。解决使用Field(min_length1)约束列表最小长度。案例3字符串字段超长导致下游截断现象summary字段 Agent 写了 500 字下游展示时被截断。根因没有在 Pydantic Field 中限制字符串最大长度。解决使用Field(max_length100)限制长度。4.5 思考题如果一份简历需要多个 Agent 协同评估技术评估 文化匹配 薪资匹配你是让每个 Agent 输出独立的 Pydantic 模型还是让最后一个 Agent 汇总成一个大模型两种方案在维护性、可靠性和扩展性上的差异是什么当 Pydantic 解析失败时CrewAI 会触发重试。但如果连续 3 次重试都失败比如模型就是输出不了合法的枚举值你该如何设计降级方案是切换为更宽松的 JSON 模式还是直接人工接管答案将在后续章节揭晓。延伸阅读与资源10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析