做Agent开发这几年我常用一个笨办法来检验自己对某个技术点的掌握程度把一个真实遇到的小需求做成项目而不是只跑通官方示例。这个系列到第4个项目我选了“结构化输出问答器”——名字看起来不起眼但它同时把Agent开发里最容易踩坑的三件事揉在了一起结构化输出、长文本处理和流程可靠性。这篇文章把我从设计到落地、从翻车到调通的完整过程记录下来希望能给正在做Agent项目的人一些参考。先说清楚这个问答器是干什么的。输入是一篇文档、一份会议纪要、一个网页文章甚至是一堆零散的笔记输出是一批高质量的问答对每条问答都带难度标签、知识点分类和原文出处。关键不是“让AI随便回答几个问题”而是让输出的结果严格符合预设的数据结构可以直接入库、可以对接下游评分系统也可以拿来生成复习材料或者客服话术库。适合谁来看如果你正在学Agent开发想搞明白结构化输出到底怎么落地或者你手头有一批资料要批量转成题库、话术库这篇文章里的方案可以直接抄作业。1. 项目思路与整体设计1.1 先用一句话定义这个Agent我给自己定的目标是输入任意长度的文本输出一份结构完全可控的JSON问答集。听起来简单真做起来问题就来了。文本长度怎么处理问答数量是固定还是动态难度标签怎么定义答案必须是原文内容还是允许AI归纳这些问题如果不提前定清楚代码写到最后全是if else打补丁。所以我在设计阶段先画了一个最小流程五个节点串起来输入清洗去掉无关字符、统一格式文本分块按固定长度切段保留上下文重叠生成候选问答每个分块交给大模型生成若干条候选结构校验检查JSON是否符合预定义schema不合格的字段标出来合并去重跨块去重、排序、输出最终结果这五步里第3步是“Agent”的本职工作第4步才是这个项目的灵魂。很多人在做类似工具时有个误区觉得只要在Prompt里写一句“请输出JSON格式”就算结构化输出了。真用起来你会发现大模型偶尔会把JSON嵌在解释性的文字里偶尔会多一个字段偶尔会把difficulty的值写成中级而不是medium。所以校验和修复机制必须做成流程的一部分而不是指望模型自觉。1.2 结构化输出为什么值得单独做我拿个生活化的例子解释一下结构化输出的价值。你让一个实习生帮你整理资料他说“我把要点写在纸上了”这叫自由文本输出你给他一张表格告诉他“每一列填什么、格式是什么、不合格的打回重填”这叫结构化输出。自由文本方便人看但机器处理不了下游每接一个系统就要重新解析一次结构化输出虽然写起来麻烦一点但它让结果变成“数据”可以被程序直接消费。具体到问答器这个场景结构化输出的收益尤其明显。第一下游可以直接消费。我要把问答对导入答题小程序接口要求就是{question, answer, difficulty}三个字段少了或者格式变了入库直接报错。第二可以自动化质检。机器能对每个字段做规则校验比如检查答案长度、检查问题是否重复这在纯文本结果下根本没法做。第三可以追踪溯源。我给每条问答都加了source_chunk_index字段标注它出自哪一段原文这条在审核和纠错时特别有用哪条是幻觉一眼就能定位。2. 技术选型与关键取舍2.1 Agent框架怎么选LangGraph、Dify和CrewAI的对比说实话被问到“Agent框架哪个好”这个问题我每次都头大。因为选框架这件事本质上是在选“你要放弃什么”。我这次需要的是可控的图结构、灵活的校验节点以及对数据结构有强约束力所以选了LangGraph。框架核心优势适合场景不适合的场景LangGraph图编排可控性强节点粒度细复杂流程、需要自定义校验/重试逻辑不想写代码、想纯配置Dify可视化编排接入快原型验证、业务人员自助搭建对schema精确控制要求高的场景CrewAI多角色协作任务拆分自然需要多个Agent扮演不同角色协同单Agent流水线反而显得重我拿Dify也试过一轮。它的界面编排确实快但到了“对输出结果做精确校验并触发重新生成”这一步可视化拖拽就有点力不从心了。你只能在节点之间写一些简单的判断复杂逻辑要么写代码插件要么回归到Prompt里让模型自己别出错。CrewAI我也看了一下它的强项是角色扮演比如一个“研究员”Agent负责提取知识点、一个“出题人”Agent负责写问题。这个思路很诱人但对本项目来说角色切换的额外Token开销有点不值。所以我最终坚持LangGraph宁可代码多写几行也要把整个流程的“闸门”握在自己手里。2.2 结构化输出的三种实现路径实现“让模型输出JSON”的方式行业里大致有三条路Prompt硬约束在系统提示词里写“你必须只输出JSON不要输出任何其他内容”。便宜但稳定性靠运气复杂schema下出错率不低。函数调用/工具调用把“生成问答对”包装成一个函数让模型在调用函数时填参数。兼容性好主流的API基本都支持但参数太复杂时模型会漏填。原生结构化输出模式也就是各厂商的JSON模式或Structured Outputs模型直接按给定的schema返回。准确性最高但部分API厂商需要单独计费或有限制。我最终的方案是原生结构化输出优先函数调用兜底。也就是第一遍跑Structured Outputs让模型按Pydantic模型出结果如果API不支持或者输出校验失败再退回函数调用重试。这么设计不是因为函数调用不好用而是因为原生结构化输出在字段完整性上确实高一个档次尤其字段嵌套、枚举值校验这种场景能省掉很多补丁代码。2.3 Schema设计是隐藏的核心工作量很多人做类似项目把精力全放在Agent流程上Schema随便画两笔就开工结果是后面返工最多的地方。我这次设计Schema时踩了不少坑总结下来有三个要点。第一个要点是字段语义要足够明确。比如difficulty字段如果只写“难度等级”四个字模型会给出各种五花八门的答案。我直接在字段描述里写“easy表示原文直接给出答案的常识性问题medium表示需要原文中两处以上信息归纳hard表示需要推理”准确率立刻不一样。第二个要点是能枚举的字段不要留字符串。category虽然看起来应该自由填写但我还是给了一个候选集合否则20条问答能给你分出18个分类来合并的时候想哭。第三个要点是留一个容错字段。我给每条问答都加了一个可选的note字段模型觉得某些信息不确定时可以写在这里而不是硬塞进错误的字段里——这招对减少幻觉乱填非常有效。3. 实操过程与核心实现3.1 环境准备与项目结构这次项目我用的技术栈是Python 3.11、LangGraph 0.2、Pydantic 2、大模型API用的是OpenAI兼容接口。用兼容接口是因为它不是唯一选择方便换不同的供应商我本地的测试环境也可以直接切到开源模型上。依赖安装很简单两条命令的事pip install langgraph langchain-openai pydantic项目的目录结构我按“流程节点”来组织而不是按传统MVC来组织qa_agent/ ├── schema.py # 输出结构化定义 ├── nodes.py # 各节点逻辑 ├── workflow.py # LangGraph图编排 ├── preprocess.py # 文本清洗与分块 ├── validate.py # JSON校验与修复 └── main.py # 入口与示例3.2 先用Pydantic把“标准答案”定下来我习惯先写Schema因为它是整个流程的“契约”。Schema定了生成节点知道要什么格式校验节点知道拿什么标准去查。代码如下from pydantic import BaseModel, Field from typing import List, Literal, Optional class QAItem(BaseModel): question: str Field( description问题。必须基于原文信息不能凭空编造 ) answer: str Field( description答案。优先引用原文原句引用时保留关键信息 ) difficulty: Literal[easy, medium, hard] Field( descriptioneasy可直接从原文找到答案medium需整合两处以上内容hard需推理或综合 ) category: Literal[概念, 流程, 数据, 结论, 实践] Field( description知识点的分类 ) source_chunk_index: int Field( description该条问答对应的原文分块编号 ) note: Optional[str] Field( defaultNone, description补充说明或不确定项可选 ) class QAOutput(BaseModel): items: List[QAItem] summary: str Field( description整篇文档的知识点概述不超过200字 )这里有个细节值得说我把source_chunk_index放进Schema是整个项目里最值的一笔设计。最初我并没有这个字段结果模型生成的答案我根本不知道它从哪来的人工审核时一条条去翻原文特别痛苦。加了这个字段后我能写脚本自动核对答案里的关键词是否真出现在对应分块里直接过滤掉了一部分幻觉。3.3 构建Agent工作流从单次调用到可修复的循环问答器的核心工作流我把它编排成四个节点from typing import TypedDict from langgraph.graph import StateGraph, END class AgentState(TypedDict): input_text: str chunks: list candidates: list validation_errors: list final_output: dict retry_count: int def build_graph(): workflow StateGraph(AgentState) workflow.add_node(split, split_node) workflow.add_node(generate, generate_node) workflow.add_node(validate, validate_node) workflow.add_node(repair, repair_node) workflow.set_entry_point(split) workflow.add_edge(split, generate) workflow.add_edge(generate, validate) workflow.add_conditional_edges( validate, decide_next, # 返回 repair 或 done {repair: repair, done: END} ) workflow.add_conditional_edges( repair, decide_retry, # 返回 validate 或 END {validate: validate, done: END} ) return workflow.compile()generate_node做的事情比较直接就是把分块文本和Schema一起交给模型要求按结构化输出格式返回结果。但真正让这个Agent“活”起来的是validate和repair这一对节点组成的反馈回路——如果校验不通过repair_node会把错误信息拼进Prompt里让模型在下一次生成时专门修复这些字段最长重试两次。这比一次性让模型“尽量做对”要可靠得多因为模型在知道具体错误时修正成功率会明显更高。在写repair_node时我犯过一个典型错误一开始把整个文本和全部错误塞回去让模型重新生成结果Token消耗巨大还出现“把对的也改错了”的情况。后来改成只把错误的那几条问答和对应的原文片段送回去修复成本降了一多半效果也稳定了。3.4 分块策略为什么直接切段行不通长文本处理是这个项目里另一个容易翻车的地方。直接按固定字符切可能会把一句话从中间切断导致模型对这个片段的理解不完整。我用的方案是带重叠的滑动窗口每块600到800字相邻两块重叠100字左右。这样即使知识点跨越了边界模型在相邻块里仍然能看到上下文。分块的代码不长大家可以直接拿来用def split_text(text: str, chunk_size: int 600, overlap: int 100) - list[str]: if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end start chunk_size chunk text[start:end] chunks.append(chunk) if end len(text): break start end - overlap return chunks还需要处理两个细节。一是尽量在段落边界上切而不是硬生生从字符中间切。我简化处理成“在chunk_size附近找一个最近的换行符作为切点”实测下来问答质量明显更好。二是如果输入本身就有标题结构比如文档里有##级别的小标题我会优先在小标题处切分保证每个分块在语义上是完整的模块。3.5 校验逻辑不要相信模型的任何一次输出validate_node里做的事看似简单实际上包含了好几个层次的检查。第一层是JSON结构是否能被Pydantic解析字段有没有缺、类型对不对第二层是枚举值检查difficulty必须落在三个合法值里第三层是内容级检查比如问题的长度、答案是否为空、source_chunk_index是否在有效范围内。第三层检查尤其重要。我见过很多次这类情况模型输出了一个格式完美的JSON但内容是错的——它把原文里没有的东西硬编成了答案。格式对但内容不对这是最隐蔽的坑因为校验代码通常只查结构不查语义。我的应对办法是加入了“答案与原文相关性检查”把[答案中的关键名词短语]和[对应分块的文本]做相似度计算低于阈值就标记为可疑进入修复流程或者直接丢弃。这样虽然不能保证100%消除幻觉但能过滤掉明显跑偏的条目。4. 运行中的典型问题与排查实录4.1 JSON校验失败的主要情形整理一下我实际运行时遇到最多的三类问题以及对应的处置方式问题现象根因解决办法返回结果里带了解释性文字比如“好的以下是...”模型没被足够强约束给generate_node的Prompt加“禁止输出任何非JSON内容”切到结构化输出模式difficulty字段返回了“容易”而不是“easy”枚举描述不够清晰在Schema描述里给每个枚举值配示例修复时把错误值和合法值一起给模型字段缺失比如source_chunk_index没有模型遗漏了不显眼的字段在后处理时默认填充当前分块编号同时把缺失字段列表放进修复Prompt这里想强调一点**同样的错误第一次出现时靠Prompt约束第二次还出现就要靠校验和修复流程兜底。**不要指望一次Prompt调好所有问题尤其是切换模型供应商之后——同一个Prompt在模型A上很稳定到模型B上可能就变了个样。4.2 重复问答并归排序的细节跨多个分块生成问答一个明显的问题是重复。文档里讲了三次“缓存击穿”模型就在三个分块里都生成了类似的问题。我最初用一个简单的字符串相似度去重结果太激进把一些真的不同的问题也干掉了。后来改成两级判断先按category分组组内再比较“问题去掉停用词后的编辑距离”距离低于0.3才视为重复。这个阈值不是拍脑袋定的我跑了100条样本反复调出来的。阈值太低会漏掉重复项太高会误删好问题。实际使用中如果你的文档专业性较强术语重复率高建议把阈值适当提高一点比如0.4。另外去重之后最好保留与原文source_chunk_index最接近的那条因为它的“上下文亲缘性”更好。4.3 上下文长度与成本控制长文档一次性塞给模型要么超上下文窗口要么Token费用猛涨。分块虽然解决了第一个问题但也会带来“同一个主题被重复生成与检索”的成本浪费。我的优化包括生成前对分块做去重压缩标记高度雷同的段落对category明确的可跳过块直接在Prompt里说明“如果该块没有可出的题允许输出空列表”。还有一个小技巧是把summary字段单独放在最后一个节点生成。全文概述需要看完整篇文章和生成问答放在一起会占用大量上下文。我在流程的最后单独加了一个summarize节点只让它读取所有分块的要点而不是让每个分块都带着负担生成概述。4.4 Agent安全与数据边界热词里“Agent安全”被频繁提及这个点在实际项目中不是口号而是会直接影响质量的工程约束。我在设计时做了几件事一是对输入内容做白名单检查禁止超出指定域名的链接内容进入Agent二是对模型输出做脱敏检查防止它把内部敏感信息复述出来三是限制重试次数防止生成循环失控产生大量成本。还有一点容易被忽略Agent的工具调用日志要留存。问答器每次调用模型、每次校验失败、每次修复重试都该把关键信息记下来。我踩过一次坑一个问答批次出现大量格式错误但我当时没留日志根本不知道是哪一步、哪个提示词版本造成的排查了半天。加了日志之后这类问题基本能在一分钟内定位。5. 如何提升问答质量的几个实操心得5.1 Few-shot示例比字段描述更管用前文提过字段描述应该写清楚、写具体。但有些边界情况一个字段描述解决不了这时候Few-shot示例的价值就体现出来了。我在Schema旁边给了一个示例问答对——一条“概念”类难度的完整JSON一条“实践”类的示例——模型照着模仿的稳定性比我写三行字段描述高得多。尤其当你的问答器要面向某个特定领域时给两个该风格的示例输出的调性会明显拉回来。这个示例怎么选我推荐从你的实际语料里挑两条有代表性的、人工改好的问答作为样本。因为模型真正需要学习的是你这个场景里“什么算好答案什么算坏答案”的潜规则。5.2 温度参数结构化输出场景下请压低生成问答时我把温度设置在0.2左右。温度太高模型会在措辞上“发挥”导致同一批文档里问题风格飘忽温度太低又容易让所有问题都长一个样缺少层次。实测0.2到0.3是一个甜点区既能保持格式稳定又不至于让问题变成同一句话的模板。如果你做的不是“出题”而是“创意提问”可以适当调到0.5以上但这时候你就得接受后处理成本的上升。5.3 用自动评测集来防止回归项目的最后我做了一个小评测集选了10篇不同风格的文档人工标注了总共120条理想问答。每次改动Prompt或逻辑后我都在这个评测集上跑一遍统计格式通过率和内容相关性得分。这个习惯帮我避免了好几次“改好了A问题引入了B问题”的回归。自动评测不用做得多复杂两个指标就够Pydantic解析通过率以及抽样人工复核通过与不过的比例。6. 一些后续可以扩展的方向这个问答器目前的形态已经能直接用于日常场景了比如把产品文档批量转成FAQ把课程讲义转成练习题组甚至把聊天记录整理成模拟面试题。但如果你想把项目再往前推进我有几个建议方向。一个是接入RAG增强。现在Agent靠的是“分块后原文生成”如果内容量大到需要检索可以先用向量库检索相关片段再在这些片段上生成问答效率和上限都会更高。另一个方向是交互式追问让用户先上传资料Agent生成初版问答后用户可以逐条要求Agent修订这需要引入一个“记忆”节点来记住用户对每条问答的修改意见。还有一个扩展是支持多模态输入比如从PPT和图片中提取文字再做问答生成——这正好接上Agent项目里经常涉及的多模态解析场景。做这个项目的过程中我最大的体会是Agent开发里最难的其实不是“让模型回答问题”而是把回答变成可以被系统信任和消费的产物。结构化输出就像给模型装上了一层“接口契约”有了这层契约Agent才能从玩具变成工具。如果你也在做类似的Agent项目强烈建议先从这个问答器练手——它足够小一天能做完又足够典型几乎涵盖了Agent工程化的所有基础概念。真做完你以后再上手更复杂的Agent项目心里会踏实很多。