对于大型语言模型有很多热议,但我对不断涌现的聊天机器人感到颇为失望,它们只是说你想听的话。我对AI的期待远不止于此。我希望它能帮我处理任务,并帮助我梳理数字世界,让我的生活更加从容自在。
最近我了解到Gemini的结构化输出API功能。它可以根据输入的模式返回一个基于JSON的数据,而不是像以前那样用一堆纯文本来回应你的多模态输入。
所以我可以利用 Gemini 将非结构化输入转换成结构化输入,并以此为基础编写更好的程序。
例如,我经常读书。今年我读了28本书。我用Goodreads来追踪我的阅读情况,不过Goodreads现在已经没有API了。我也是纽约公共图书馆的常用户,它也不提供API。我希望能有一个系统,能自动从我的Goodreads待读书单中,向图书馆请求借阅。
为此,我想要设定一套标准格式来表示Goodreads和NYPL上的图书。除了图书之外,我还希望大型语言模型能够结构化地理解任何内容。
所以我转向了schema.org,这是一个几年前启动的一个项目,旨在帮助定义语义网络。这些类型在网络中已被广泛应用,并为各种对象提供一个通用词汇表,包括书。
我可以轻松地从他们GitHub仓库中的这个单一JSON文件提取所有的现有模式。这种格式是JSON,尽管它通过大量链接和属性来表示每个属性或类型。
{
"@id": "schema:illustrator",
"@type": "rdf:Property",
"rdfs:comment": "这本书的插画师。",
"rdfs:label": "illustrator",
"schema:domainIncludes": {
"@id": "schema:Book"
},
"schema:rangeIncludes": {
"@id": "schema:Person"
}
},
{
"@id": "schema:Book",
"@type": "rdfs:Class",
"rdfs:comment": "一本普通的书。",
"rdfs:label": "书",
"rdfs:subClassOf": {
"@id": "schema:CreativeWork",
"说明": "创意作品"
}
},
这并没有特别有帮助,因为需要将其转换成更标准的 JSON 架构格式以符合 Gemini API 的需求。
于是我就写了一个简短的脚本来处理这个文件。
const schemas = require('../src/schemaorg-jsonld.json')
const graph = schemas['@graph']
const geminiGraph: Record<string, Schema> = {}
const possibleTypes: string[] = []
const simpleDataTypes = {
'schema:Text': SchemaType.STRING,
'schema:URL': SchemaType.STRING,
'schema:Boolean': SchemaType.BOOLEAN,
'schema:Number': SchemaType.NUMBER,
'schema:Integer': SchemaType.INTEGER,
'schema:Time': SchemaType.STRING, // 时间字符串
'schema:DateTime': SchemaType.STRING, // 时间字符串
'schema:Date': SchemaType.STRING, // 时间字符串
}
// 第一次遍历为图中的每个项目创建一个条目
for (const property of graph) {
const id = property['@id']
if (simpleDataTypes[id]) {
geminiGraph[id] = {
type: simpleDataTypes[id],
description: property['rdfs:comment'] || property['rdfs:label'] || "",
}
continue
}
geminiGraph[id] = {
type: simpleDataTypes[property['@type']] ?? SchemaType.OBJECT,
description: property['rdfs:comment'] || property['rdfs:label'] || "",
}
// 这样就开始了内存映射的过程
if (!simpleDataTypes[property['@type']]) {
geminiGraph[id].properties = {}
}
if (property['rdfs:subClassOf']) {
if (property['rdfs:subClassOf']['@id'] === 'schema:Enumeration') {
// 这是一个枚举
geminiGraph[property['@id']].type = SchemaType.STRING
geminiGraph[property['@id']].enum = []
continue
}
}
}
// 下一次遍历将图中的条目连接起来
for (const property of graph) {
if (geminiGraph[property['@id']].type !== 'object') {
continue
}
const potentialTypes: string[] = (() => {
const sp = property['schema:rangeIncludes']
if (sp === undefined) return [0] // 如果 sp 未定义则返回 [0]
if (Array.isArray(sp)) {
return sp.map(x => x['@id'])
}
return Object.values(sp)
})()
const type = potentialTypes[0]
// for (const type of potentialTypes) {
const id = (() => {
const origId = property['@id']
if (potentialTypes.length === 1) {
return origId
}
// return `${origId}_${type}`
return `${origId}`
})()
geminiGraph[id] = {...geminiGraph[property['@id']]}
if (property['schema:rangeIncludes']) {
const ref = simpleDataTypes[type] ?
{ type: simpleDataTypes[type] } :
geminiGraph[type]
if (geminiGraph[id].properties) {
geminiGraph[id].properties![type] = ref
} else {
geminiGraph[id].properties = {
[type]: ref
}
}
}
const superProperties: string[] = (() => {
const sp = property['schema:domainIncludes']
if (sp === undefined) return []
if (Array.isArray(sp)) {
return sp.map(x => x['@id'])
}
return Object.values(sp)
})()
for (const superProperty of superProperties) {
if (!geminiGraph[superProperty].properties) {
geminiGraph[superProperty]!.properties = {
[id]: geminiGraph[id]
}
} else {
geminiGraph[superProperty]!.properties![id] = geminiGraph[id]
}
}
// }
}
// 枚举遍历
for (const property of graph) {
const ptype = property['@type']
if (geminiGraph[ptype]?.enum !== undefined) {
geminiGraph[ptype].enum?.push(property['rdfs:label'])
}
}
function subclassPass(property: any) {
const log = false
const subClassOf = (() => {
const sp = property['rdfs:subClassOf']
if (sp === undefined) return []
if (Array.isArray(sp)) {
return sp.map(x => x['@id'])
}
return Object.values(sp)
})()
if (log) { console.log('sarr', subClassOf) }
for (const sco of subClassOf) {
if (log) { console.log(sco) }
if (log) { console.log(geminiGraph[sco]) }
if (sco === 'schema:Enumeration') return
if (!geminiGraph[sco]) continue
const graphSco = graph.find(x => x['@id'] === sco)
if (graphSco['rdfs:subClassOf']) {
if (log) { console.log('scp', sco) }
subclassPass(graphSco)
}
const superclass = geminiGraph[sco]
if (!superclass || !superclass.properties) continue
if (!geminiGraph[property['@id']].properties) {
geminiGraph[property['@id']].properties = {}
}
for (const [k, v] of Object.entries<Schema>(superclass.properties ?? {})) {
const pid = property['@id']
if (log) { console.log('kv', k) }
geminiGraph[pid]!.properties![k] = v
}
}
}
// 子类遍历过程
for (const property of graph) {
if (!property['rdfs:subClassOf']) continue
subclassPass(property)
}
const schema = 'schema:Book'
const jsonout = geminiGraph[schema]
console.log(JSON.stringify(jsonout))
花了点时间反复调整才达到这个阶段,使用内存引用确保顺序不影响结果。最后我再做了一次调整,确保枚举正常工作。当然这并不是最高效的脚本,但我只需要在预处理阶段运行一次即可,将所有类型转换完成。之后在实际运行中就能用上了。
逐行运行脚本时,一切正常。当我尝试使用 JSON.stringify
时,却遇到了一个意外的错误。错误信息提到有循环引用。这是怎么一回事。
schema.org 类型的问题在于它们太过于灵活且强类型。这在某些方面非常好,但这也使得数据序列化变得复杂。
如果你查看 Book 类型,你会注意到有像 Boolean abridged
和 Text isbn
这样的属性。它继承了来自 CreativeWork 类型的属性,例如 Number copyrightYear
,以及来自 Thing 类型的属性,例如 Text name
。这也意味着它会带入很多不太关键的属性,比如 Text interactivityType
。这使得类型变得非常冗长,但这些不太重要的属性可以被忽略,也不是什么大事。
更大的问题是这样的,一本书有一个叫做 Person illustrator
的属性,它指向一个标准的 Person 类型。这种 person 类型有一个叫做 Person children
的属性,表示该人物的后代。当你尝试序列化整个结构时,如果包含了这个人的后代,就会引发无限递归的问题。这确实是个大问题。
可惜的是,也没有太好的解决办法。我对于这个想了很多。
有一次,我尝试将很多类型硬编码为字符串类型。因为虽然我的书有一个Person
插画师挺不错,但这意味着我最终可能会存储插画师的children
,这些children
的deathPlace
,这些deathPlace
的review
,以及review
的copyrightHolder
等等,诸如此类。
const simpleDataTypes = {
'schema:Text': SchemaType.STRING,
'schema:URL': SchemaType.STRING,
'schema:Boolean': SchemaType.BOOLEAN,
'schema:Number': SchemaType.NUMBER,
'schema:Integer': SchemaType.INTEGER,
'schema:Time': SchemaType.STRING, // 时间戳
'schema:DateTime': SchemaType.STRING, // 时间戳
'schema:Date': SchemaType.STRING, // 时间戳
'schema:ListItem': SchemaType.STRING, // 列表项
'schema:DefinedTerm': SchemaType.STRING, // 定义术语
'schema:Taxon': SchemaType.STRING, // 处理方式
'schema:BioChemEntity': SchemaType.STRING, // 处理方式
'schema:DefinedTermSet': SchemaType.STRING, // 处理方式
'schema:ImageObject': SchemaType.STRING, // 处理方式
'schema:MediaObject': SchemaType.STRING, // 处理方式
'schema:TextObject': SchemaType.STRING, // 处理方式
'schema:VideoObject': SchemaType.STRING, // 处理方式
'schema:AudioObject': SchemaType.STRING, // 处理方式
'schema:Language': SchemaType.STRING, // 处理方式
'schema:QuantitativeValue': SchemaType.NUMBER, // 数量值
'schema:AboutPage': SchemaType.STRING, // 处理方式
'schema:Audience': SchemaType.STRING, // 处理方式
'schema:Claim': SchemaType.STRING, // 处理方式
'schema:Comment': SchemaType.STRING, // 处理方式
'schema:bioChemInteraction': SchemaType.STRING, // 处理方式
'schema:bioChemSimilarity': SchemaType.STRING, // 处理方式
'schema:hasBioChemEntityPart': SchemaType.STRING, // 处理方式
'schema:softwareAddOn': SchemaType.STRING, // 处理方式
'schema:worksFor': SchemaType.STRING, // 处理方式
'schema:parents': SchemaType.STRING, // 处理方式
'schema:advanceBookingRequirement': SchemaType.STRING, // 处理方式
'schema:potentialAction': SchemaType.STRING, // 处理方式
'schema:publisherImprint': SchemaType.STRING, // 处理方式
'schema:subjectOf': SchemaType.STRING, // 处理方式
'schema:offeredBy': SchemaType.STRING, // 处理方式
'schema:interactionType': SchemaType.STRING, // 处理方式
'schema:address': SchemaType.STRING, // 处理方式
'schema:spatial': SchemaType.STRING, // 处理方式
'schema:geoTouches': SchemaType.STRING, // 处理方式
'schema:sourceOrganization': SchemaType.STRING, // 处理方式
'schema:mainEntityOfPage': SchemaType.STRING, // 处理方式
'schema:isBasedOnUrl': SchemaType.STRING, // 处理方式
'schema:servicePostalAddress': SchemaType.STRING, // 处理方式
'schema:publishedOn': SchemaType.STRING, // 处理方式
'schema:diversityStaffingReport': SchemaType.STRING, // 处理方式
'schema:archivedAt': SchemaType.STRING, // 处理方式
'schema:publishingPrinciples': SchemaType.STRING, // 处理方式
'schema:occupationLocation': SchemaType.STRING, // 处理方式
'schema:educationRequirements': SchemaType.STRING, // 处理方式
'schema:performerIn': SchemaType.STRING, // 处理方式
'schema:correctionsPolicy': SchemaType.STRING, // 处理方式
'schema:hostingOrganization': SchemaType.STRING, // 处理方式
'schema:composer': SchemaType.STRING, // 处理方式
'schema:funding': SchemaType.STRING, // 处理方式
'schema:recordedAt': SchemaType.STRING, // 处理方式
'schema:material': SchemaType.STRING, // 处理方式
'schema:license': SchemaType.STRING, // 处理方式
'schema:usageInfo': SchemaType.STRING, // 处理方式
'schema:producer': SchemaType.STRING, // 处理方式
'schema:countryOfOrigin': SchemaType.STRING, // 处理方式
'schema:exampleOfWork': SchemaType.STRING, // 处理方式
'schema:workExample': SchemaType.STRING, // 处理方式
'schema:hasCertification': SchemaType.STRING, // 处理方式
'schema:hasCredential': SchemaType.STRING, // 处理方式
'schema:containedIn': SchemaType.STRING, // 处理方式
'schema:department': SchemaType.STRING, // 处理方式
'schema:makesOffer': SchemaType.STRING, // 处理方式
'schema:translationOfWork': SchemaType.STRING, // 处理方式
'schema:serviceSmsNumber': SchemaType.STRING, // 处理方式
'schema:subEvent': SchemaType.STRING, // 处理方式
'schema:eventSchedule': SchemaType.STRING, // 处理方式
'schema:shippingOrigin': SchemaType.STRING, // 处理方式
'schema:validForMemberTier': SchemaType.STRING, // 处理方式
'schema:openingHoursSpecification': SchemaType.STRING, // 处理方式
'schema:geoCrosses': SchemaType.STRING, // 处理方式
'schema:contributor': SchemaType.STRING, // 处理方式
'schema:accountablePerson': SchemaType.STRING, // 处理方式
'schema:affiliation': SchemaType.STRING, // 处理方式
'schema:funder': SchemaType.STRING, // 处理方式
'schema:alumniOf': SchemaType.STRING, // 处理方式
'schema:brand': SchemaType.STRING, // 处理方式
'schema:memberOf': SchemaType.STRING, // 处理方式
'schema:recordedIn': SchemaType.STRING, // 处理方式
'schema:deathPlace': SchemaType.STRING, // 处理方式
'schema:homeLocation': SchemaType.STRING, // 处理方式
'schema:workLocation': SchemaType.STRING, // 处理方式
'schema:locationCreated': SchemaType.STRING, // 处理方式
'schema:spatialCoverage': SchemaType.STRING, // 处理方式
'schema:attendee': SchemaType.STRING, // 处理方式
'schema:workFeatured': SchemaType.STRING, // 处理方式
'schema:workPerformed': SchemaType.STRING, // 处理方式
'schema:itemOffered': SchemaType.STRING, // 处理方式
'schema:availableAtOrFrom': SchemaType.STRING, // 处理方式
'schema:parentOrganization': SchemaType.STRING, // 处理方式
'schema:manufacturer': SchemaType.STRING, // 处理方式
'schema:isRelatedTo': SchemaType.STRING, // 处理方式
'schema:birthPlace': SchemaType.STRING, // 处理方式
'schema:character': SchemaType.STRING, // 处理方式
'schema:illustrator': SchemaType.STRING, // 处理方式
'schema:sponsor': SchemaType.STRING, // 处理方式
'schema:author': SchemaType.STRING, // 处理方式
'schema:creator': SchemaType.STRING, // 处理方式
'schema:editor': SchemaType.STRING, // 处理方式
'schema:maintainer': SchemaType.STRING, // 处理方式
'schema:provider': SchemaType.STRING, // 处理方式
'schema:translator': SchemaType.STRING, // 处理方式
'schema:publisher': SchemaType.STRING, // 处理方式
'schema:sdPublisher': SchemaType.STRING, // 处理方式
'schema:seller': SchemaType.STRING, // 处理方式
'schema:contentLocation': SchemaType.STRING, // 处理方式
'schema:publishedBy': SchemaType.STRING, // 处理方式
'schema:director': SchemaType.STRING, // 处理方式
'schema:directors': SchemaType.STRING, // 处理方式
'schema:attendees': SchemaType.STRING, // 处理方式
'schema:founder': SchemaType.STRING, // 处理方式
'schema:members': SchemaType.STRING, // 处理方式
'schema:actor': SchemaType.STRING, // 处理方式
'schema:actors': SchemaType.STRING, // 处理方式
'schema:organizer': SchemaType.STRING, // 处理方式
'schema:copyrightHolder': SchemaType.STRING, // 处理方式
'schema:musicBy': SchemaType.STRING, // 处理方式
'schema:partOfEpisode': SchemaType.STRING, // 处理方式
'schema:partOfSeason': SchemaType.STRING, // 处理方式
'schema:partOfSeries': SchemaType.STRING, // 处理方式
'schema:productionCompany': SchemaType.STRING, // 处理方式
'schema:performer': SchemaType.STRING, // 处理方式
'schema:performers': SchemaType.STRING, // 处理方式
'schema:eligibleTransactionVolume': SchemaType.STRING, // 处理方式
'schema:superEvent': SchemaType.STRING, // 处理方式
'schema:subEvents': SchemaType.STRING, // 处理方式
'schema:video': SchemaType.STRING, // 处理方式
'schema:workTranslation': SchemaType.STRING, // 处理方式
'schema:isPartOf': SchemaType.STRING, // 处理方式
'schema:hasPart': SchemaType.STRING, // 处理方式
'schema:isVariantOf': SchemaType.STRING, // 处理方式
'schema:isSimilarTo': SchemaType.STRING, // 处理方式
'schema:isAccessoryOrSparePartFor': SchemaType.STRING, // 处理方式
'schema:predecessorOf': SchemaType.STRING, // 处理方式
'schema:successorOf': SchemaType.STRING, // 处理方式
'schema:model': SchemaType.STRING, // 处理方式
'schema:isConsumableFor': SchemaType.STRING, // 处理方式
'schema:sdLicense': SchemaType.STRING, // 处理方式
'schema:warranty': SchemaType.STRING, // 处理方式
'schema:hasProductReturnPolicy': SchemaType.STRING, // 处理方式
'schema:hasMerchantReturnPolicy': SchemaType.STRING, // 处理方式
'schema:mentions': SchemaType.STRING, // 处理方式
'schema:educationalAlignment': SchemaType.STRING, // 处理方式
'schema:about': SchemaType.STRING, // 处理方式
'schema:mainEntity': SchemaType.STRING, // 处理方式
'schema:additionalProperty': SchemaType.STRING, // 处理方式
'schema:interactionStatistic': SchemaType.NUMBER, // 互动统计
}
结果事情有点失控,我不断发现需要将更多字段强制转换为字符串,而不是富类型。
不幸的是,即使做了这么多工作,仍然无法得到一个合理的结果。拥有这么多复杂的模式类型导致了不断寻找下一个循环引用的斗争。
我尝试了几种库来解决循环问题,也尝试断掉所有循环引用,但最后还是会得到一个乱七八糟且很长的 JSON 字符串,Gemini 处理起来比较棘手。这是在 AI Studio 上经过几小时尝试后决定放弃这种方法的时候的截图。
经过一番挣扎,我终于决定我的方法从根本上讲是错误的。虽然我可以将Schema类型和字段作为初始参考点,但我不能直接使用它们仓库中的类型。我必须从零开始重新构建它们,并确保它们可以被正确序列化,以便它们可以被正确序列化。
const simpleString = (description: string) => ({
type: SchemaType.STRING,
description,
})
const simpleBool = (description: string) => ({
type: SchemaType.BOOLEAN,
description,
})
schemaGraph['bookFormatType'] = simpleEnum('书籍的出版格式。', [
'AudiobookFormat', 'EBook', 'GraphicNovel',
'Hardcover', 'Paperback',
])
schemaGraph['Book'] = {
type: SchemaType.OBJECT,
properties: {
...schemaGraph['creativeWork'].properties,
abridged: simpleBool('表示该书是否为缩印本。'),
bookEdition: simpleString('该书的版本。'),
bookFormat: schemaGraph['bookFormatType'],
illustrator: simpleString('该书的插画者。'),
isbn: simpleString('该书的ISBN号。'),
numberOfPages: simpleInt('该书的页码数。'),
}
}
最后,我可以从Goodreads上截图,Gemini能够正确地将截图转换成高质量的JSON数据。
但我不仅仅如此。我还需要一个初步处理,将输入获取为截图中对象的实际类型。这可能不总是 Book。所以我需要从所有这些标题格式的类型中创建一个枚举,并用它作为初始的提示。
const typeEnums = {
type: SchemaType.STRING,
description: '最能代表输入的最优类型',
enum: Object.keys(schemaGraph).过滤(x => {
const x0 = x.substr(0, 1)
return x0 === x0.toUpperCase()
})
}
因为它正确地识别了截图中的书籍,所以可以继续执行第二个查询,获取书籍的模式和JSON输出。
这篇最初的帖子详细介绍了我早期使用AI来开始构建一个更新的语义网的工作。通过使用大语言模型将非结构化数据转化为结构化数据,我可以更轻松地继续推进我的工作。通过标准化有限类型的集合,我可以将不同的网站连接起来,并使数据迁移更加容易。
很快我就会再写一篇帖子,来跟进这项初始工作以及我学到的东西。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章