如何从大型语言模型中获取可靠的结构化数据
大多数关于如何调用语言模型的教程都会在 JSON.parse(response.content) 这行代码处结束。这段代码在处理前十个测试用例时确实可以有效运行。但当你开始实际应用时,会在第400次左右的一次请求中遇到问题:模型可能会返回一个它自己编造出来的日期,或者当你的数据结构应该包含5个元素时却返回8个数组项,又或者返回一个格式完全正确但实际上缺少某个字段的JSON对象。 我在开发Temploracraft这个简历工具时遇到了这样的问题。这个工具会接收用户上传的文档,并将其转换成应用程序可以编辑的结构化数据。 输入的数据确实具有很大的不可预测性:有些是两列结构的PDF文件,有些表格其实并
大多数关于如何调用语言模型的教程都会在JSON.parse(response.content)这行代码处结束。这段代码在处理前十个测试用例时确实可以有效运行。但当你开始实际应用时,会在第400次左右的一次请求中遇到问题:模型可能会返回一个它自己编造出来的日期,或者当你的数据结构应该包含5个元素时却返回8个数组项,又或者返回一个格式完全正确但实际上缺少某个字段的JSON对象。
我在开发Temploracraft这个简历工具时遇到了这样的问题。这个工具会接收用户上传的文档,并将其转换成应用程序可以编辑的结构化数据。
输入的数据确实具有很大的不可预测性:有些是两列结构的PDF文件,有些表格其实并不符合常规的表格格式,还有日期的表示方式多达14种不同形式。而输出的数据必须严格符合要求,因为每一个被提取出来的字段最终都会显示在用户能够看到的界面上。如果模型判断日期错误,用户会在大约两秒钟内发现这个问题。
这篇文章探讨的就是介于“模型返回了一些文本”和“我的应用程序获得了可以信任的数据”这两个阶段之间的那层处理机制。它介绍了三种限制输出结果的方法,解释了为什么先设计数据结构比编写更长的提示语更为重要,说明了如何构建一个不会浪费资源的重试机制,以及对于那些无论如何尝试都无法解决的错误应该采取什么措施。
目录
先决条件
为了能够顺利跟随这篇文章的学习进程,你需要具备以下条件:
具备TypeScript的相关知识:示例代码中会轻微使用类型推断和泛型功能,你应该能够毫不费力地理解这些内容。
至少曾经调用过一次语言模型API:你不需要成为这方面的专家,但应该了解系统提示语的含义以及令牌的基本概念。
熟悉JSON Schema也是有帮助的,但并非必需:
在讲解相关内容时,我会解释其中重要的部分。如果想运行示例代码,建议使用Node.js 18或更高版本:
因为这些示例代码使用了内置的fetch函数和异步迭代器功能。
这些代码示例使用了Zod来进行模式定义,同时利用Anthropic SDK来调用模型,但这里的每种技术方法都可以直接应用于其他验证库或其他服务提供商。真正重要的是这些理念本身,而非具体的软件包。
为什么仅通过请求JSON数据是不够的
当需要结构化数据时,人们的第一反应通常是礼貌地提出请求。你会写下类似“请返回符合此结构的有效JSON格式的数据,且不要包含任何其他文本”这样的要求,然后提供一个示例,这样通常就能解决问题。在开发过程中也是如此,在演示中同样适用。
问题在于,语言模型是根据概率逐个生成字符的,而你的指令只是众多影响因素之一。它还要与模型的训练机制、输入文档的结构,以及模型在前几百个字符中生成的内容进行竞争。大多数情况下,你的指令会得到执行;但偶尔也会失败。
以下是我在实际生产环境中收集到的一些错误案例,这些错误都是发生在被明确要求返回严格符合JSON格式的数据的模型身上:
尽管被两次告知不要这样做,该模型仍然将响应内容用Markdown代码格式进行了封装。
它将
"2019 - Present"这个日期字符串作为一个整体返回了,而实际上模式中明确要求分别设置startDate和endDate字段。对于文档中明确标记为“当前日期”的字段,该模型却生成了
"2024-12-31"这个结束日期。它返回的字符串是
"null",而不是真正的空值对象。对于模式中规定最多只能包含五个项目的列表项,该模型却生成了七个项目。
由于响应内容的长度超出了限制,该模型将数据截断了。
需要注意的是,并非所有这些错误都属于同一类型。使用Markdown代码格式进行封装或数据被截断属于语法问题,这类问题通常可以通过局部修改代码来解决,而无需重新调用模型;将日期字段合并成一条字符串或生成了多余的列表项则是模式匹配问题,虽然生成的JSON在语法上是正确的,但并不符合预期的结构;而生成错误的结束日期则属于语义理解错误,这样的输出虽然在格式上符合要求,但在内容上与原始数据不符。
针对这三类问题,需要采取不同的处理方式。将它们混为一谈,是我在提取代码中经常看到的常见设计错误。本文的后续部分主要讨论如何区分和解决这些问题。
约束输出结果的三种方法
在编写任何验证代码之前,首先应该了解API本身能够为你提供哪些保障。目前共有三种机制,它们各自提供了不同的保证效果。
JSON模式是这三种机制中最弱的一种。你只需设置一个标志,比如response_format: { type: "json_object" },服务提供商就会确保返回的响应在语法上是有效的JSON格式。这一点确实很有用,因为它能一次性解决使用Markdown代码格式或数据被截断的问题。但这种机制并不能保证输出数据的结构符合要求。因此,你可能会得到键值对错误、数据类型不正确,或者结构完全不符合预期的有效JSON结果。
以下是使用Anthropic SDK时的具体实现示例:
const response = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 4096,
tools: [
{
name: "emitresume",
description: "以结构化数据的形式返回解析后的简历内容。",
input_schema: jsonSchema,
},
],
tool_choice: { type: "tool", name: "emit.resume" },
messages: [{ role: "user", content: resumeText }],
});
const block = response.content.find((b) => b.type === "tool_use");
const candidate = block?.input;
tool_choice字段是关键所在。如果没有这个字段,模型就会自行决定是否使用某个工具;有时它甚至会直接用普通文本来回答问题。而通过指定tool_choice字段,就可以强制模型使用特定的工具。
OpenAI通过严格的结构化输出实现了这种机制;如果你在本地运行模型,也可以在lama.cpp中使用GBNF语法,或者借助Outlines这样的库来实现相同的功能。
不过,受限解码也存在一定的风险:如果模式中要求某个字段而文档中实际上并不存在这个字段,模型也无法拒绝执行请求,因此它就会用其他内容来填充这个空缺。这样一来,解析失败就会转化为难以察觉的“幻觉”结果。正因为如此,我才会将某些必填字段设置为可为空值,这一点我稍后会再详细说明。
我的默认做法是使用包含大量可为空值的模式,并在此基础上进行验证。这种组合方式能够带来大部分好处,同时也能避免出现各种问题。
先制定模式,再编写提示语
当输出质量不佳时,人们往往会认为写一条更长的提示语就能解决问题——多举一些例子、加重强调语气、多使用大写字母等等。这种方法确实能在一定程度上有所帮助,但效果并不持久,因为提示语终究只是普通文本,而普通文本是无法被强制执行的。
更好的做法是将模式视为最重要的基础文件,让其他所有内容都基于它来生成。只需定义一次模式,就可以用这个定义来生成TypeScript类型、发送给API的JSON格式数据,以及运行时验证工具。一旦模式的结构发生变化,这三者都会随之同步调整,从而确保它们之间不会出现矛盾。
在Zod框架中,具体的实现方式就是这样的:
import { z } from "zod";
const YearMonth = z
.string()
.regex /^\d{4}-\d{2}$/, "期望输入的日期格式为YYYY-MM");
const Role = z.object({
company: z.string().min(1),
title: z.string().min(1),
startDate: YearMonth,
endDate: YearMonthnullable(),
bullets: z.array(z.string().min(1)).min(1).max(5),
});
const Resume = z.object({
name: z.string().min(1),
email: z.string().email().nullable(),
roles: z.array(Role),
});
export type Resume = z.infer; 其中有三项细节起到了实质性的作用。
YearMonth正则表达式的范围比z.string()要窄,而这种限制正是关键所在。如果将字段定义为纯字符串类型,模型很可能会返回“January 2019”、“2019 - Present”或“01/2019”这样的值,而这些格式都是符合验证规则的。但在模式层面对格式进行明确限定,就能确保错误会立即被发现,而不会在处理日期数据的代码中经过多层处理后才被发现。
endDate字段被定义为可为空值而非可选字段——这一改变显著提升了数据提取的质量。对于可选字段,模型可以随意忽略它;但在这种情况下,你无法区分“文档中没有提供该信息”和“模型忘记了该信息”。而将字段设置为可为空值,则会迫使模型做出明确的处理决策,而null这个值本身就意味着该职位仍然是当前的。
在描述列表项时使用max(5)这一限制,实际上是将具体的规则直接编码到数据结构中,而不是在后续处理过程中才对数组进行截取。如果模型生成的列表项数量超过了这个限制,你就应该及时发现这个问题,因为这通常意味着模型并没有真正提取到有效的数据,而是在填充空缺内容。
要想将数据结构发送给API,就需要根据相同的定义生成JSON格式的描述文件:
import { zodToJsonSchema } from "zod-to-json-schema";
const jsonSchema = zodToJsonSchema(Resume, { target: "openApi3" });
有一个习惯值得养成:对于那些仅凭名称就容易引起误解的字段,一定要编写.describe()>描述语句。这些描述会被包含在JSON格式的描述文件中,从而作为工具定义的一部分传递给模型,这样它们就能起到针对特定字段提供明确指导的作用。
const Role = z.object({
company: z.string().min(1),
title: z.string().min(1).describe("这是个人的职位名称,而不是团队名称"),
startDate: YearMonth,
endDate: YearMonthnullable().describe("如果该职位是当前的,那么这个字段可以设置为null"),
bullets: z
.array(z.string().min(1))
.min(1)
.max(5)
.describe("请直接使用文档中给出的原文,不要进行任何修改或总结."),
});
最后那条描述规则有效地避免了一类错误——在这种情况下,模型可能会自行改进个人列表项中的表述内容。
验证工作分为两部分,而不仅仅是一步而已
一旦收到响应数据,人们往往会直接使用schema.parse()>方法就认为验证工作已经完成。Zod确实能够判断数据结构是否正确,但如果只是结构上的验证的话,那还不够。
结构验证和语义验证其实是两回事,而只有前者是免费提供的。Zod可以告诉你endDate字段是否是一个符合YYYY-MM格式的字符串,但它无法判断这个结束日期是否早于开始日期,或者这个日期是否在未来的某个时间点,也无法判断被标记为“当前职位”的职位是否真的有结束日期。这些都属于语义验证的范围,而所有这些情况都是错误的。
因此,需要进行两次验证:第一次是结构验证,它基于数据结构本身来进行;第二次则是根据你对这个领域的了解来进行的进一步检查。function findSemanticProblems(resume: Resume): string[] {
const problems: string[] = [];
const currentMonth = new Date().toISOString().slice(0, 7);
for (const role of resume.roles) {
if (rolestartDate > currentMonth) {
problems.push(`${role.company}: 开始日期在将来`);
}
if (role endDate && role.endDate < role startDate) {
problems.push(${role.company}: 结束日期早于开始日期`);
}
}
const currentRoles = resume.roles.filter((r) => rendDate === null);
if (currentRoles.length > 1) {
problems.push("有多个职位被标记为当前职位");
}
return problems;
}
这些代码并没有什么高明的之处,而这恰恰就是其意义所在。它们只是普通的业务逻辑,只不过用来检查模型是否正确工作而已,并非用于验证用户的输入信息。在发现错误时再编写这样的代码吧,把每次新的检查都视为对模型之前所犯错误的永久性回归测试。
这里存在一种有用的不对称性:对于语义上的错误来说,直接将它们呈现给用户通常比重新运行模型来得更有效,因为模型利用现有信息往往无法得到更好的结果。如果一份文档确实列出了两个当前职位,那么这就是文档所显示的内容,再次让模型进行判断也不会改变这一结果。
构建一个不会消耗令牌的重试循环
当验证失败时,人们通常会想用相同的输入再次调用模型,期望得到不同的结果。这种做法在很多情况下确实有效,但也会造成资源的浪费——因为对于那些已经成功完成大部分处理的请求来说,用户仍然需要支付全部的输入成本。
做出两个小小的调整就能产生巨大的效果。
首先,在花费任何资源之前,应该先尝试进行本地修复。很多错误其实只是表面上的问题,通过简单的字符串处理就可以解决:
function salvage(raw: string): string {
let text = raw.trim();
// 删除模型添加的Markdown标记。
const fenced = text.match/^```(?:json)?\s*([\s\S]*?)\s*$$$/);
if (fenced) text = fenced[1];
// 删除文档开头或结尾的冗余文本。
const start = text.indexOf("{");
const end = text.lastIndexOf("}");
if (start !== -1 && end > start) text = text.slice(start, end + 1);
return text;
}
其次,当确实需要再次调用模型时,应该发送修复请求而不是重新开始整个处理流程。在请求中应包含原始的输入内容、失败的输出结果以及具体的验证错误信息。模型已经完成了大部分的数据提取工作,现在只需要它来解决那些具体存在的问题而已。这样修复操作会更快完成,产生的输出也会更短,因此所需的资源也会更少。
async function requestRepair(
originalPrompt: string,
badOutput: string,
error: z.ZodError,
): Promise {
const issues = error.issues
.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`)
.join("\n");
const response = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 4096,
messages: [
{ role: "user", content: originalPrompt },
{ role: "assistant", content: badOutput },
{
role: "user",
content:
`该输出因以下问题未能通过验证:\n${issues}\n\n` +
`请仅返回修正后的JSON格式内容,保留所有已经正确的部分。",
},
],
});
return response.content[0].type === "text" ? response.content[0].text : "";
}
综合来看,这个循环机制的作用是区分不同的失败类型,并限制尝试次数:
type Outcome =
| { ok: true; value: T; attempts: number }
| { ok: false; error: string; attempts: number };
async function extract(
schema: z.ZodType,
prompt: string,
maxAttempts = 3
): Promise> {
let lastRaw = "";
let lastError: z.ZodError | null = null;
for (let attempt = 1; attempt <= max Attempts; attempt++) {
lastRaw =
attempt === 1
? await callModel(prompt)
: await requestRepair(prompt, lastRaw, lastError!);
let candidate: unknown;
try {
candidate = JSON.parse(salvage(lastRaw));
} catch {
continue; // 如果解析失败,直接跳过本次尝试。
}
const result = schema.safeParse(candidate);
if (result.success) {
return { ok: true, value: result.data, attempts: attempt };
}
lastError = result.error;
}
return {
ok: false,
error: lastError?.message ?? "输出数据无法被解析",
attempts: maxAttempts,
};
}
关于这个循环机制,有两点设计意图是显而易见的。首先,它会返回尝试次数,这个数据值得被记录下来,因为每次成功所需的尝试次数是评估提取流程效率的重要指标。其次,它将尝试次数限制在三次以内——如果三次尝试都没有得到有效的结果,再进行第四次尝试往往也无济于事,此时选择优雅地退出程序反而比继续浪费资源更好。
还需要注意的一点是:速率限制错误与模式验证错误属于不同的失败类型,因此不应该采用相同的重试策略。对于速率限制错误,应该采用指数级退避算法,因为问题出在时间控制上;而对于模式验证错误,则需要立即发送修复请求,因为问题出在数据内容本身,等待并不会改变任何情况。
流式结构化输出
流式输出与结构化输出之间存在矛盾。流式输出的设计目的是让用户能够在响应完成之前就看到处理进度,但只有当所有的闭合括号都被解析出来后,才能真正生成一个有效的JSON对象。如果提取数据需要12秒钟的时间,那么最好的处理方式就是让用户等待这12秒钟,或者想办法以流式格式展示一些有意义的信息。
目前有两种合理的处理方法。
第一种方法是使用部分JSON解析器,这种解析器会接收不完整的字符串,并尝试从中提取出最有效的结构,同时会补全缺失的闭合括号并忽略那些不完整的数据。像best-effort-json-parser这样的库就采用了这种方法。当输出数据确实是一个大的对象时,这种方法是可行的;但它的缺点是,中间状态可能会产生误导,因为某些字段可能会以被截断的形式出现,从而看起来像是完整的。
第二种方法更适合当输出数据是一组列表时的情况。这时可以将输出格式改为每行包含一个对象,这样就可以实现流式输出了。由于每一行的数据都可以独立进行解析,因此可以在数据解析完成的瞬间就对其进行验证并展示出来。
const stream = client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 4096,
messages: [{ role: "user", content: prompt }],
});
let buffer = "";
for await (const event of stream) {
if (event.type !== "content_block_delta") continue;
buffer += event.delta.text ?? "";
const lines = buffer.split("\n");
buffer = lines.pop() ?? ""; // 保留未完成的部分,以便处理下一块数据。
for (const line of lines) {
if (!line.trim()) continue;
try {
const parsed = Role.safeParse(JSON.parse(line));
if (parsed.success) onRole(parsed.data);
} catch {
// 如果遇到格式错误的行,就直接忽略它,而不会导致整个解析过程失败。
}
}
}
在处理数据缓冲区时,很多人会犯错。网络传输过来的数据块与行界并不一致,因此某个数据块往往会在某个对象的中途结束。将最后一个元素重新添加到缓冲区中,并继续进行处理,这样才能确保程序能够正确地处理剩余的数据。
这种处理方式可以让你对每一条数据进行单独的验证,这比直接解析一个大型数据对象要有优势得多。因为如果其中有一条数据有错误,也不会影响其他数据的解析结果。
那些无法通过重试来解决的故障
到目前为止,我们所讨论的所有情况都假设:只要你的请求方式正确,模型就能产生正确的结果。但有些故障并不是这样,如果将这些故障视为可以重试的情况,不仅会浪费资源,还会得到错误的数据。
其中最常见的一种故障就是从源数据中提取那些根本不存在的信息。例如,如果一份简历中确实没有电子邮件地址,而你的数据结构却要求必须包含这一字段,那么模型就会生成一些看似合理但实际上错误的值。
即使重新尝试,也会得到不同的“看似合理”的结果。对于这种故障,如果采用强制解码的方式,情况只会变得更糟,因为语法规定某个应该为空的字段必须包含某种格式正确的数据。解决这个问题的方法是在数据结构层面进行修改,因此在我的数据提取方案中,几乎所有的字段都被设置为可为空。
源数据中的歧义也是导致类似问题出现的另一个原因。当一份文档中同时列出了两个不同的职位名称以及相应的日期范围时,实际上并没有正确的提取方式,只能进行猜测。重新尝试虽然会得到不同的猜测结果,但置信度仍然是一样的。对于这类情况,数据结构中应该包含表示置信度的信息,用户界面中也应该提供审核机制,而不是再通过一次API调用来解决这个问题。
还有一种情况是数据被无声地截断了。如果响应数据在某个对象的中途就达到了输出字符数的限制,那么就会得到格式错误的JSON数据。如果使用简单的重试机制,程序会再次尝试解析,但同样的问题仍然会再次出现。因此,在尝试重试之前,一定要检查响应数据中显示的停止原因。如果模型是因为字符数不足而停止工作的,那么不增加字符数限制或不对输入数据进行分割,重新尝试肯定还会失败。
一般来说,在决定是否应该重试之前,首先需要判断这种故障属于哪种类型,这样才能确定重试是否真的有助于解决问题。
这实际上会带来哪些代价
每一层处理都对应一定的成本,因此对其进行量化测量是必要的,而不能仅凭猜测来决定。
需要重点关注的关键指标是每次成功提取所需尝试的次数。请在每次请求时记录这一数据,并重点观察其p95分位数而非平均值,因为平均值会掩盖那些真正产生成本的部分。当数据库模式发生变化导致质量下降时,这个数值会比其他指标更早出现变化。
在这里,提示信息的缓存效果比在大多数工作负载中都要重要得多。提取提示信息非常适合进行缓存处理,因为在每次请求时,系统提示、数据库模式以及少量的示例数据都是完全相同的,只有输入文档会发生变化。
在一个处理流程中,如果数据库模式和指令涉及的字符数量达到数千个,那么将相关数据放入缓存机制中,将会大幅降低输入处理成本。而且,每次重试时都会再次利用这些缓存数据,因此这种节省效果会更加明显。
为什么修复请求的成本会比重新尝试的成本更低呢?原因在于:虽然输入数据的量会增加——因为需要发送原始提示信息、失败的输出结果以及错误列表——但输出数据的量实际上会大大减少,因为模型只需要修改其中少数几个字段,而不需要重新生成整个文档。在大多数服务提供商那里,生成输出数据才是成本较高的环节,因此这种处理方式通常来说是划算的。
还有两件事从一开始就值得进行监控:一是验证错误出现的分布情况,这一数据能准确指出是哪些数据库字段导致了问题,其参考价值远高于整体失败率;二是本地修复机制的成功率。如果salvage()函数能够修复大部分错误结果,那就说明提示信息本身存在问题,这时只需解决一次这个问题即可,而不需要反复进行修复操作。
当你不需要这些功能时
这些技术只在特定的使用场景下才具有价值,在其他情况下则属于过度设计。
如果模型的输出结果会直接由人类阅读,而且人们会将其视为普通文本来理解,那么你根本不需要结构化输出格式。聊天界面、摘要或电子邮件草稿等工具本身就已经有人类来进行验证了,再添加数据库模式只会给模型带来额外的限制,而并不会带来任何实际好处。
如果你只需要提取一两个字段的数据,而不是整个文档的内容,那么使用针对性强的提示信息、适当的safeParse>安全处理机制以及一次重试就足够了。只有当处理量非常大或者数据库模式非常复杂时,才需要使用重试循环、语义验证层和流式解析器这些功能。
如果你的处理量很小,而且每项结果都会由人类进行审核,那么验证环节实际上是在重复别人已经在做的事情。直接提供原始输出结果,让审核人员来进行修改,把开发资源用在其他更有价值的地方吧。
最后,如果你还在探索这个功能应该具备哪些具体功能,那么就暂时不要急于构建这些系统。因为一旦提取代码、验证规则和存储的数据都依赖于数据库模式,那么修改它就会带来巨大的成本。先进行初步设计,了解你真正需要哪些字段,然后再逐步完善系统即可。
最安全的做法是先制定一个数据结构规范,然后使用safeParse函数进行解析;只有当确实出现故障时,才需要添加后续的处理层。本文中介绍的每一种技术都是基于具体的生产实践中的问题而总结出来的,并非源自设计文档。
总结
一个可正常运行的演示版本与一个真正可靠的功能之间的差距,在很大程度上就体现在这一处理环节上。模型本身已不再是难点,提示语通常也不是问题所在;真正的难点在于如何确定自己能够接受什么结果、如何在发现结果不符合预期时及时察觉,并在那种情况下采取合理的应对措施。
以下三个原则具有至关重要的意义:
数据结构规范就是契约,所有后续的设计都应以此为依据。如果一个定义能够同时生成你的数据类型、API规范以及验证器,那么这三者就永远不会出现矛盾或分歧。
要将语法错误、数据结构规范错误与语义错误区分开来。它们产生的原因不同,解决方法也各不相同;如果使用相同的重试机制来处理这些错误,就会把资金浪费在那些通过重复调用无法解决的问题上。
应将字段设置为“可为空”而非“可选”,而不是允许系统默认省略这些字段。这样就能把那些原本被隐藏起来的问题明确地暴露出来,从而便于进行验证。
只要把这些原则掌握好了,剩下的就是常规的工程实践了。你需要编写验证代码和重试逻辑,而这些工作开发者们几十年来一直在处理不可靠的输入数据时都在做。语言模型只不过是一种新型的“不可靠输入”,而采用同样的规范和管理方法,它也能很好地被控制。
参考资料
Zod:这些示例中使用的数据结构规范库。
zod-to-json-schema:将Zod数据结构规范转换为API所期望的JSON格式。
Anthropic工具使用指南:如何通过工具定义来强制输入数据必须符合特定的结构要求。
OpenAI的结构化输出功能:通过严格的解码规则来确保输入数据符合规范。
- :为本地运行的模型提供基于语法规则的生成功能。
- :在响应数据仍在传输的过程中,能够解析不完整的JSON数据。
相关文章
使用OpenTelemetry实现Claude Code的可观测性
像 Claude Code 、 OpenAI Codex 、 Google Antigravity 以及 Cursor 这样的代理编码工具,在日常软件开发中已经变得无处不在。 随着代理系统的不断发展,开发者让这些系统完成的大部分工作都是通过逐个分配子任务来实现的。许多团队也在探索并使用共享的、多租户式的代理基础设施,这种架构的成本不会与某个特定的所有者挂钩。在这种情况下,可观测性就成为了监控基础设施成本的关键因素。 在本指南中,您将了解可观测性的工作原理,然后学习如何启用Claude Code内置的遥测功能,运行后端程序来收集数据,并读取该系统生成的各类指标、日志及追踪信息。这些内容将帮助您更
阅读全文
如何测试Flutter应用程序:单元测试、组件测试、黄金标准测试以及集成测试详解
第一次在技术面试中被问到“你的测试覆盖范围是多少?”时,我并没有一个令人满意的答案。 那时我已经发布了几款真正的Flutter应用程序,它们可以正常运行,用户也在使用它们。但我的测试工作其实非常有限——仅仅是为某个定价功能编写了少量的单元测试而已,并没有其他测试内容。 几个月后,我对其中一个任务完成流程进行了重构,这个修改在代码差异对比中看起来完全没问题,但却破坏了用户真正关心的一个功能:当用户将某项任务标记为已完成时,该任务并不会从错误的列表中移除。虽然没有任何程序崩溃,也没有任何错误日志被记录下来,但用户却开始不再信任这款应用程序。而我直到有用户把这个问题告诉了一位也是测试人员的朋友,才发
阅读全文
从数据到价值:通过一个实际应用案例来理解数据管理[完整书籍]
如今,数据已成为一种极其宝贵的资源。它使企业能够在市场中展开竞争,并推动创新,从而提升所提供产品与服务的质量。 数据处理能让团队自动化各种流程。它还能辅助决策制定,为终端用户带来更加个性化的体验,在诸如银行欺诈防范或风险控制等诸多领域帮助人们发现其中的规律。企业必须明白如何以有效、安全且合法的方式收集和利用数据。 你很可能目前正在使用或曾经使用过各种各样的产品与服务。你也知道,那些涉及数据的流程几乎与我们身边的所有事物都息息相关。此外,你或许已经熟悉“大数据”、“数据分析”、“人工智能”以及“机器学习”这些术语了。 但除非你是这些领域中的专家,否则其中一些概念可能会让你感到难以理解。毕竟,这些
阅读全文
如何使用Python和Neo4j构建知识图谱【完整指南】
你所处理的大部分数据实际上都反映了各种关系:一个客户属于某个账户,某次事件会影响某种服务,而一名工程师负责维护某个代码库。你把所有这些信息存储在表格中,长期以来,这种存储方式一直运行得非常顺利。 然而,有时会有人提出这样的问题: 哪些工程师最近了解过昨晚那次事件所影响的服务情况? 这类问题很容易理解,但编写相应的SQL查询却相当困难。通常需要使用四到五次连接操作,每次连接都会生成一个比最终结果范围更广的中间数据集,而大部分这些中间数据最终都会被丢弃。随着表格规模的扩大,查询速度会变得越来越慢,而且每次查看这个查询语句时,都很难理解其具体逻辑。 为了解决这类问题,人们才创造了图数据库。 在这本手
阅读全文