JSON 混入思考草稿
Perplexity 流式结构化输出把 think 内的 JSON 草稿当结果。拦住未闭合思考区块,保留字段验证,并在流结束后明确核验完整文本。
Perplexity 的 reasoning 模型先输出 <think>,里面写了一份准备放弃的 JSON 草稿。LangChain 的流式结构化输出却已经把那份草稿交给调用方,甚至变成通过 Pydantic 校验的对象;最终答案到来后,字段才变成另一组值。
这条路径涉及 langchain-perplexity 1.4.1 的 ReasoningJsonOutputParser 和 ReasoningStructuredOutputParser。已核对的正式实现会删除完整 <think>...</think> 区块,但流还没有收到结束标签时,区块内容仍会进入后面的解析器。
合法对象也可能只是思考中的候选
例如思考中先出现 {"city": "Paris"},随后否定它,真正答案是 {"city": "Tokyo"}。完整文本解析可以移除思考区块,流式处理中却可能先输出 Paris。
字段名与类型正确,并不能让草稿变成最终答案。Pydantic 检查对象形状,不知道这个对象来自仍未闭合的思考区块。不能收到一个校验通过的 City,就立即保存订单、更新状态或执行工具。
核对原始文本片段与流式对象的时间关系:提前结果是否来自 <think> 内的代码围栏,结束标签是否还没到达。普通的不完整 JSON、网络截断或其他模型内容格式,需要另外区分。
在解析边界挡住未闭合的区块
业务只需要最终对象时,可以沿现有链改用完整结果调用,在生成完成后解析。需要保留流式预览的应用,则应在进入结构化解析之前处理思考区块,不能让后面的 JSON 容错从它里面挑出一份草稿。
下面是针对“标准思考区块位于输出开头”的 JSON parser 包装,使用已核对版本的公开类与函数;它只定义解析器,不发送模型请求:
from langchain_core.exceptions import OutputParserException
from langchain_perplexity.output_parsers import (
ReasoningJsonOutputParser,
strip_think_tags,
)
class ClosedReasoningJsonParser(ReasoningJsonOutputParser):
def parse_result(self, result, *, partial=False):
stripped = strip_think_tags(result[0].text)
if stripped.startswith("<think>"):
if partial:
return None
raise OutputParserException("reasoning block not closed")
return super().parse_result(result, partial=partial)
流式 partial 阶段遇到这个未闭合区块时,不发布候选 JSON。完整结果按 partial=False 解析仍未闭合时,明确失败;仅消费流不会自动补做这次最终解析,调用方应在完成后显式核验完整文本。完整思考区块被移除后,原 JSON parser 继续处理真正答案,不改变原来的字段内容。
这只是 JSON 解析的包装。使用 Pydantic schema 的链应在原结构化 parser 上保留同样的前置边界,不能换成普通 JSON parser 后丢掉字段验证。包装器也需要实际接入原链,定义了类却仍沿默认 with_structured_output 路径解析,没有改变结果。
示例限定标准开头标签,不是任意嵌套标签、其他 reasoning 格式或恶意文本的通用解析器。模型流中没有明确区块边界时,应按实际协议整理输入,不能用一个 startswith 代替全部格式判断。
区块关闭之后,仍有 partial 状态
关闭思考区块,只说明现在解析的是答案区域;答案本身仍可能尚未完成。预览可以展示阶段性字段,业务提交仍需等待完整生成、正常完成状态与最终字段验证。
验收保留思考区块内的合法 JSON 草稿和随后不同的最终答案,检查草稿没有作为答案对象输出;再覆盖结束标签未到达就停止、正常最终 JSON 和字段验证失败。不能只用一段没有 <think> 的 JSON,证明原来的问题已经消失。


