ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用 CocoIndex 把商品目录变成推荐图谱:LLM 分类提取 + Neo4j 增量构建实战

用 CocoIndex 把商品目录变成推荐图谱:LLM 分类提取 + Neo4j 增量构建实战 用 CocoIndex 把商品目录变成推荐图谱LLM 分类提取 Neo4j 增量构建实战【免费下载链接】cocoindexIncremental engine for long horizon agents Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/co/cocoindex本文以 examples/product_recommendation 示例为核心讲解如何用 CocoIndex 把一批商品 JSON 自动改造成一张可查询的 Neo4j 推荐图谱LLM 负责给每个商品打上它是什么和买它还需要什么两类分类标签共享的分类节点与两类关系边共同构成买了 A 的人还需要 B的推荐引擎而这一切运行在普通的异步 Python 中。读完本文你将掌握 CocoIndex 两阶段流水线的写法、coco.fn(memoTrue)的增量重算机制、Neo4j 图目标节点表 / 关系边的挂载方式以及用一条 Cypher 查询输出跨售推荐的方法。核心思路推荐信息就藏在商品描述里一份商品列表里往往隐含着天然的推荐关系——一支钢笔和墨芯、笔记本搭在一起一台显示器和支架、HDMI 线搭在一起。但这些知识锁在商品描述的自然语言里人工整理不现实静态规则又难以覆盖千变万化的商品。示例的思路是让 LLM 从商品详情文本中提取两类分类标签再把标签之间的共享关系建成一张图推荐结论直接从图里查出来不需要单独训练或部署任何推荐模型。整个变换在原生 Python 里声明形如target_state transformation(source_state)增量处理、变更跟踪、图目标管理等重活由底层的 Rust 引擎承担因此编辑一个商品只会重新提取一个商品而不是整份目录。示例的入口是 examples/product_recommendation/main.py全文只有一个文件、约 280 行是理解 CocoIndex 声明式流水线的极佳范本。图模型两类节点、两类关系边推荐图谱由两种节点和两种关系组成元素类型说明Product节点节点每个商品一条标题、价格主键为文件名去掉.jsonTaxonomy节点节点每个去重后的分类标签一条如gel pen、notebook、ink refill按值作主键跨商品共享PRODUCT_TAXONOMY边关系Product → Taxonomy这个商品是什么PRODUCT_COMPLEMENTARY_TAXONOMY边关系Product → Taxonomy买这个商品的人可能还需要什么推荐逻辑因此变得非常简洁某个商品的 complementary 分类命中另一个商品的 is-a 分类两者就应当被一起推荐。在 main.py 中这两类节点和两类边分别用 dataclass 与neo4j.TableTarget/neo4j.RelationTarget声明dataclass class Product: id: str # 主键——文件名去掉 .json title: str price: float dataclass class Taxonomy: value: str # 主键——分类标签本身PRODUCT_TAXONOMY与PRODUCT_COMPLEMENTARY_TAXONOMY两条边不带额外负载连接器直接以(from_id, to_id)推导每条边的主键因此同一对(产品, 标签)只会出现一条边。两阶段流水线为什么必须分两步由于 Taxonomy 标签在所有商品间共享示例把流水线拆成两个阶段自上而下阅读 main.py 即可看到全貌阶段一逐商品每个商品声明自己的Product节点调用 LLM 提取分类并把标签携带给下一阶段阶段二一次遍历由唯一一个 graph pass 统一声明去重后的Taxonomy节点以及两类关系边。如果让每个商品各自声明 Taxonomy 节点同一个gel pen就会被重复建成多个节点共享节点必须由单一阶段统一拥有这正是shared nodes, done right的关键。阶段一LLM 提取 节点声明 标签携带coco.fn(memoTrue) # 按内容缓存每次提取——只重跑发生变化的商品 async def extract_taxonomy(detail: str) - ProductTaxonomyInfo: client instructor.from_litellm(litellm.acompletion, modeinstructor.Mode.JSON) result await client.chat.completions.create( modelcoco.use_context(LLM_MODEL), response_modelProductTaxonomyInfo, messages[{role: system, content: TAXONOMY_PROMPT}, {role: user, content: detail}], ) return ProductTaxonomyInfo.model_validate(result.model_dump()) coco.fn(memoTrue) # 阶段一——逐商品声明节点、提取、携带标签 async def process_file( file: FileLike, product_table: neo4j.TableTarget[Product], ) - ProductTaxonomies: raw json.loads(await file.read_text()) product_id file.file_path.path.name.removesuffix(.json) price float(str(raw[price]).lstrip($).replace(,, )) product_table.declare_record(rowProduct(idproduct_id, titleraw[title], priceprice)) info await extract_taxonomy(PRODUCT_TEMPLATE.render(**raw)) return ProductTaxonomies( product_idproduct_id, taxonomies[t.name for t in info.taxonomies], complementary[t.name for t in info.complementary_taxonomies], )值得注意的几个实现细节价格清洗float(str(raw[price]).lstrip($).replace(,, ))能同时处理$4.99和$1,349.00这类带货币符号与千分位的原始字符串提取入参商品 JSON 不是直接丢给 LLM而是先经PRODUCT_TEMPLATEJinja2 模板渲染成结构化的 Markdown 文本标题 Highlights Description再作为detail传入结构化解耦LLM 提取逻辑extract_taxonomy与文件读取逻辑process_file是独立的coco.fn前者按detail内容 memo 化后者按文件 memo 化互不影响缓存粒度。LLM 提取的响应结构由 Pydantic 模型约束配合 instructor 的 JSON 模式class ProductTaxonomy(pydantic.BaseModel): name: str pydantic.Field( description( A concise noun (or short noun phrase) for the products core functionality, without branding or style. ... ) ) class ProductTaxonomyInfo(pydantic.BaseModel): taxonomies: list[ProductTaxonomy] pydantic.Field( descriptionTaxonomies describing what this product is. ) complementary_taxonomies: list[ProductTaxonomy] pydantic.Field( descriptionTaxonomies for complementary products a buyer of this product might also need. )TAXONOMY_PROMPT的系统提示词要求模型只返回文本中确有支撑的内容name字段的约束具体名词、小写、不带品牌与风格修饰、避免office supplies这类过宽分类而倾向pen、printer等具体分类直接决定了共享标签的质量——标签越规范跨商品共享命中率越高。阶段二一次遍历统一建共享节点与边coco.fn # 阶段二——一次遍历拥有共享 Taxonomy 节点 两类边 async def build_graph( products: list[ProductTaxonomies], taxonomy_table: neo4j.TableTarget[Taxonomy], product_taxonomy_rel: neo4j.RelationTarget[Any], complementary_rel: neo4j.RelationTarget[Any], ) - None: labels: set[str] set() for p in products: labels.update(p.taxonomies) labels.update(p.complementary) for value in labels: taxonomy_table.declare_record(rowTaxonomy(valuevalue)) for p in products: for t in set(p.taxonomies): product_taxonomy_rel.declare_relation(from_idp.product_id, to_idt) for t in set(p.complementary): complementary_rel.declare_relation(from_idp.product_id, to_idt)这里的去重逻辑一目了然先把所有商品的 is-a 与 complementary 标签并成一个set再逐一声明Taxonomy节点——保证gel pen在全图中只有一个节点所有商品都指向它。关系边用set()包裹同一商品的标签列表避免重复边。环境准备与依赖示例的依赖定义在 examples/product_recommendation/pyproject.toml[project] name product-recommendation version 0.1.0 description CocoIndex example: LLM-extract product taxonomies into a Neo4j recommendation graph. requires-python 3.11 dependencies [ cocoindex[neo4j]1.0.7, instructor1.0.0, litellm1.0.0, pydantic2.0.0, jinja23.1.0, ]环境变量模板见 examples/product_recommendation/.env.example变量默认值说明COCOINDEX_DB./cocoindex.dbCocoIndex 本地状态库路径记录增量状态与 memo 缓存OPENAI_API_KEY空OpenAI 密钥使用本地模型时可不填LLM_MODELopenai/gpt-4.1LiteLLM 模型标识可换ollama/llama3.2等任意 providerNEO4J_URIbolt://localhost:7687Neo4j Bolt 地址NEO4J_USERneo4jNeo4j 用户名NEO4J_PASSWORDcocoindexNeo4j 密码按照 README 的四步即可跑通1. 启动 Neo4jdocker run -d -p 7474:7474 -p 7687:7687 -e NEO4J_AUTHneo4j/cocoindex --name cocoindex-neo4j neo4j:5.26-community2. 配置并安装cp .env.example .env # 填入 OPENAI_API_KEY或改用 LLM_MODELollama/llama3.2 pip install -e .3. 构建图谱—— 示例自带products/目录包含 9 份示例商品清单笔、笔记本、显示器配件等cocoindex update main在 9 个示例商品上这条命令会产出9 个Product节点、约 40 个Taxonomy节点以及两类关系边。4. 在 Neo4j Browser 中探索推荐—— 打开 http://localhost:7474neo4j/cocoindex对图提问。用一条 Cypher 查询买了 A 还需要 BREADME 给出了核心的推荐查询找到所有 is-a 分类命中某支凝胶笔互补分类的商品// 推荐与任何gel pen搭配的商品 // 找到 is-a 分类命中一支笔的 complementary 分类的商品 MATCH (:Taxonomy {value: gel pen})-[:PRODUCT_TAXONOMY]-(:Product) -[:PRODUCT_COMPLEMENTARY_TAXONOMY]-(need:Taxonomy) MATCH (rec:Product)-[:PRODUCT_TAXONOMY]-(need) RETURN DISTINCT rec.title查询分两步走先沿PRODUCT_COMPLEMENTARY_TAXONOMY从一支笔走到它可能还需要的 Taxonomy 集合再沿PRODUCT_TAXONOMY反向找出所有 is-a 命中这些标签的商品最后DISTINCT去重。在示例数据上为一支笔做推荐会得到笔记本notepad和多用途纸multipurpose paper——正是期望中的跨售组合。图谱本身就是推荐器没有额外的模型没有单独的训练流程。增量更新编辑一个商品会发生什么示例最核心的价值在于增量。README 明确了两层机制coco.fn(memoTrue)按内容缓存每次 LLM 提取编辑一个商品的文件内容后只有该商品的提取会被重跑其余商品直接命中缓存不会重新调用 LLM图谱整体做差异diff更新阶段二以声明期望状态的方式工作CocoIndex 会自动新增不再存在的节点/边、删除已没有任何商品支持的节点/边。两层合起来的效果是edit one product re-extracts one product, not the catalog——改一个商品只重算一个商品而不是整份目录。对依赖 LLM 调用有成本、有延迟的流水线来说这意味着后续迭代只付出 O(1) 的增量成本。源码级原理ContextKey、挂载与 memo环境注入ContextKey与coco.lifespan连接配置和模型选择都通过上下文键注入而不是全局变量KG_DB coco.ContextKeyneo4j.ConnectionFactory LLM_MODEL coco.ContextKeystr coco.lifespan async def coco_lifespan(builder: coco.EnvironmentBuilder) - AsyncIterator[None]: builder.provide( KG_DB, neo4j.ConnectionFactory( urios.environ.get(NEO4J_URI, bolt://localhost:7687), auth( os.environ.get(NEO4J_USER, neo4j), os.environ.get(NEO4J_PASSWORD, cocoindex), ), databaseos.environ.get(NEO4J_DATABASE, neo4j), ), ) builder.provide(LLM_MODEL, os.environ.get(LLM_MODEL, openai/gpt-4.1)) yielddetect_changeTrue是诚实的缓存失效开关LLM_MODEL被声明为变更可检测一旦你在.env里把模型从openai/gpt-4.1换成ollama/llama3.2流水线会对所有商品针对新模型重新提取无需手工清缓存。coco.use_context(LLM_MODEL)在extract_taxonomy中读取该上下文。从源码看python/cocoindex/connectors/neo4j/_target.py 中的ConnectionFactory持有uri、auth、database三个连接参数按需创建带认证的异步连接池 driver并通过query(cypher, params)直接执行单条 Cypher 语句。工厂把 database 名放进连接层而非表 key因此同一个 Neo4j 集群上的不同库通过不同的ConnectionFactory/ContextKey对来寻址。挂载目标mount_table_target与mount_relation_target示例在app_main中把四个目标一次性挂载到图数据库coco.fn async def app_main(sourcedir: pathlib.Path) - None: product_table await neo4j.mount_table_target( KG_DB, Product, await neo4j.TableSchema.from_class(Product, primary_keyid), primary_keyid, ) taxonomy_table await neo4j.mount_table_target( KG_DB, Taxonomy, await neo4j.TableSchema.from_class(Taxonomy, primary_keyvalue), primary_keyvalue, ) product_taxonomy_rel await neo4j.mount_relation_target( KG_DB, PRODUCT_TAXONOMY, product_table, taxonomy_table ) complementary_rel await neo4j.mount_relation_target( KG_DB, PRODUCT_COMPLEMENTARY_TAXONOMY, product_table, taxonomy_table ) ...对照 python/cocoindex/connectors/neo4j/_target.py 的实现TableTarget.declare_record把行转成字典并校验主键存在再通过coco.declare_target_state声明该节点的期望状态RelationTarget.declare_relation则从两端TableTarget取 label 与主键字段用结构化参数而非字符串拼接绑定from_id/to_id天然避免 Cypher 注入。declare_row declare_record提供了别名两者等价。数据源localfs.walk_dir文件来源声明在app_main中files localfs.walk_dir( sourcedir, recursiveTrue, path_matcherPatternFilePathMatcher(included_patterns[**/*.json]), ) file_coros [] async for path_key, file in files.items(): file_coros.append( coco.use_mount( coco.component_subpath(file, path_key), process_file, file, product_table, ) ) products: list[ProductTaxonomies] list(await asyncio.gather(*file_coros))localfs.walk_dir递归遍历products/目录PatternFilePathMatcher只保留**/*.json每个文件挂载一个独立的process_file组件组件子路径按path_key区分最后asyncio.gather并行收集全部提取结果作为阶段二的输入。注意这里process_file返回的ProductTaxonomies只是阶段间传递的内部类型并不直接落到图上。示例数据一览examples/product_recommendation/products 下是 9 份结构统一的商品 JSON每份含title、price、highlights与description例如 p1.json{ title: Pilot G2 Premium Gel Roller Pens, Fine Point, 0.7mm, Black Ink, 2/Pack, price: $4.99, highlights: [ Smooth-writing gel ink for effortless note-taking., Comfortable rubber grip for long study sessions., Refillable design reduces waste and saves money. ], description: { header: ..., paragraph: ..., bullets: [...] } }9 个商品覆盖了笔p1、多用途纸p2、橡皮p3、螺旋笔记本p4、白板p5、双肩包p6、保温水瓶p7、太阳能笔记本p8与激光打印机p9等品类正好能演示跨品类互补推荐文具 纸张、设备 配件。换成你自己的商品目录时只要保持同样的 JSON 结构即可无需改动任何流水线代码。小结与扩展方向回顾这个示例的四个设计要点共享节点由单一 pass 拥有Taxonomy按值去重gel pen是全图唯一的节点所有商品共享指向它增量是默认行为coco.fn(memoTrue)按内容缓存 LLM 提取改一个商品只重算一个商品随后整个图谱做差异更新图即推荐器无需独立模型一条 Cypher 沿complementary → is-a路径即可输出跨售候选纯 Python、你自己的技术栈提取层是 instructor 叠加 LiteLLMLLM_MODEL可切换任意 providerOpenAI、Ollama 等没有 DSL。如果想继续深入可以在 python/cocoindex/connectors/neo4j/_target.py 中查看节点表、关系边与向量索引的完整目标实现其他图数据库FalkorDB、SurrealDB的连接器也提供了结构一致的TableTarget/RelationTarget/mount_relation_targetAPI分别见 python/cocoindex/connectors/falkordb/_target.py 与 python/cocoindex/connectors/surrealdb/_target.py同一套声明式写法可以直接迁移。把商品目录变成推荐图谱本质上就是把藏在散文里的知识结构化——增量引擎保证这个过程可以随目录一起长期演进。【免费下载链接】cocoindexIncremental engine for long horizon agents Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/co/cocoindex创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表