Skip to content

feat: add knowledge vector grounding for AI shopping - #226

Merged
lgqfhwy merged 2 commits into
feat/aishopping-field-aware-fallbackfrom
codex/aishopping-knowledge-vector
Aug 7, 2026
Merged

feat: add knowledge vector grounding for AI shopping#226
lgqfhwy merged 2 commits into
feat/aishopping-field-aware-fallbackfrom
codex/aishopping-knowledge-vector

Conversation

@lgqfhwy

@lgqfhwy lgqfhwy commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add optional Ha3KnowledgeVectorConf under the existing chat recall configuration
  • generate query embeddings through the configured Feature Store LLM configuration and run HA3/OpenSearch vector retrieval against the catalog knowledge index
  • expose retrieved knowledge candidates to the existing AI shopping Planner and strictly validate selected candidate IDs before applying their catalog terms
  • keep the existing AI shopping behavior unchanged when knowledge vector configuration is absent, and degrade to the existing Planner flow when online knowledge retrieval fails

Why

The existing field-aware fallback can only search terms produced by the Planner. User wording that does not match the catalog vocabulary can therefore still produce zero or irrelevant results. This change grounds Planner product-type extraction in values retrieved from a knowledge index built from the actual product catalog.

This is a stacked PR on top of #225. It contains only the knowledge-vector increment; the field-aware fallback remains isolated in PR1.

Implementation notes

  • embeddings are generated with the Feature Store Go SDK using FeatureStoreName and LLMConfigName; vector dimension comes from the configured model
  • knowledge search is enabled only when Ha3KnowledgeVectorConf is present
  • KnowledgeTypes is required and limited to category, categories, content, brand, tag, and title
  • the Planner may only select IDs returned by the current knowledge query; unknown or duplicate IDs are rejected
  • selected candidate values are converted into existing product_type_keywords, so the product recall path stays small and reuses field-aware fallback

Validation

  • git diff --check
  • go test ./service/aishopping ./service/recall ./algorithm/aichat ./persist/fs ./recconf -run '^$'
  • go vet ./service/aishopping ./service/recall ./algorithm/aichat ./persist/fs ./recconf
  • go build ./...
  • 1,901-query PAI-REC integration run: 0 HTTP errors; Reply zero-return UV rate improved from 2.3964% to 1.5583%, HitRate@12 from 92.2300% to 93.0747%, and result precision from 83.1680% to 84.4035%

Known repository test issue: running the unfiltered service/recall test package still fails in the pre-existing TestMultibizRecall because it constructs NewBeMultiBizRecall with a nil configuration. The package compilation gate, vet, and full repository build pass.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review by ChatGPT

return matched, nil
}

type FSClient struct {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

该函数在遍历 fsInstances map 时未加锁,而 fsInstances 是一个全局变量,可能被多个 goroutine 同时读写(例如在其他地方调用 ReloadProject 或初始化客户端),存在并发读写风险,可能导致数据竞争(data race)或运行时 panic。建议对 fsInstances 的访问进行同步保护(如使用 sync.RWMutex)。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复

func parseSearchGoodsRequest(arguments string, fieldAware bool, knowledge *knowledgeEvidence) (recallsvc.SearchGoodsRequest, error) {
var req recallsvc.SearchGoodsRequest
if !fieldAware {
return req, json.Unmarshal([]byte(arguments), &req)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

marshalSearchGoodsRequest 函数中,当 json.Marshal(req) 失败时(即 err != nil),仍会返回 payload(此时为 nil 或无效值)和错误。但代码逻辑 if err != nil || knowledge == nil || len(req.KnowledgeCandidateIDs) > 0 会导致即使 marshal 失败也直接返回 (payload, err),这可能导致后续使用无效的 payload。更严重的是,如果 err != nilpayload 是 nil,但函数仍尝试对它进行 json.Unmarshal(payload, &fields),这将引发 panic。

具体路径:

  1. json.Marshal(req) 返回非 nil 错误 → payload 为 nil
  2. 条件 err != nil 成立 → 跳过 if 块?不,实际上该条件用于提前返回,但当前逻辑是:只要 err != nilreturn payload, err,这是正确的。

然而仔细看:

payload, err := json.Marshal(req)
if err != nil || knowledge == nil || len(req.KnowledgeCandidateIDs) > 0 {
    return payload, err
}

此逻辑正确:若 marshal 失败,直接返回错误;若 knowledge 为 nil 或已有 KnowledgeCandidateIDs,也直接返回原始 marshal 结果。

但问题在于:当 err == nilknowledge != nillen(req.KnowledgeCandidateIDs) == 0 时,才会进入后续逻辑。这部分逻辑是安全的。

再检查:json.Unmarshal(payload, &fields)payload 此时一定非 nil 且有效,因为前面已确保 err == nil

因此,该函数逻辑实际无 bug

但另一个潜在问题是:marshalSearchGoodsRequestknowledge == nil 时直接返回原始 marshal 结果,而调用方 normalizeFieldAwareToolCalls 传入的 knowledge 来自参数,可能为 nil。需确认调用处是否保证 knowledge 非 nil。

查看调用点:

  • normalizeFieldAwareToolCallsrunAgentLoop 调用,传入的 knowledgerunAgentLoop 的参数,该参数由上层传入。
  • runAgentLoopknowledge 参数在函数开头被用于 messagesWithKnowledgeFieldAwareSearchGoodsTool(knowledge.candidateIDs()),若 knowledge 为 nil,则 knowledge.candidateIDs() 会 panic。

因此,若 knowledge 可能为 nil,则多处会 panic。但变更前代码未使用 knowledge,变更后新增了对其的解引用(如 knowledge.candidateIDs()knowledge.apply(&req) 等),若调用方可能传入 nil,则会导致空指针 panic

关键点:runAgentLoop 新增参数 knowledge *knowledgeEvidence,并在多处直接使用其方法或字段,例如:

  • knowledge.candidateIDs()
  • messagesWithKnowledge(..., knowledge)(内部可能使用)
  • dispatchTool(..., knowledge, ...) → 最终调用 knowledge.apply(&req)

如果 knowledge 为 nil,这些调用都会 panic。

结论:本次变更假设 knowledge 非 nil,但函数签名允许传入 nil。若上层可能传入 nil,则存在空指针解引用风险。由于变更引入了对 knowledge 的多次解引用而未做 nil 检查,存在空指针 panic 的明确风险

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已确认无问题,不需要修改

result = append(result, knowledgeMessages...)
result = append(result, messages[1:]...)
return result
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

messages 非空但长度为 1 时,messages[1:] 是合法的(不会越界),但逻辑上存在潜在问题:代码假设 messages[0] 是用户消息或可直接保留的首条消息,而将知识相关消息插入到第一条之后。然而,如果原始 messages 的第一条已经是 system 消息,这种插入方式可能导致多个 system 消息顺序混乱,但这属于业务逻辑问题,非明确 Bug。

更严重的问题是:messages 为空时,函数返回 knowledgeMessages;但当 messages 非空时,强制取 messages[0] 并拼接后续消息。如果调用方传入的 messages 为 nil,则 len(messages) == 0 成立,会返回 knowledgeMessages,看似安全。然而,在 Go 中,对 nil slice 执行 len() 是安全的,因此此处无 panic。

但注意:evidence.promptJSON 被直接拼接到字符串中,若其内容不可信且最终被渲染到前端,可能存在 XSS 风险。不过,由于上下文是作为 LLM 的 system prompt 输入,通常不直接输出到 Web 页面,因此是否构成 XSS 取决于后续使用方式。在缺乏更多信息的情况下,暂不视为明确安全漏洞

综上,最值得关注的问题是:当 messages 长度为 0 时返回 knowledgeMessages,而长度 ≥1 时将 messages[0] 作为第一条,再插入知识消息。但如果 messages[0] 是 user 或 assistant 角色,而 knowledgeMessages 是 system 角色,这可能违反某些 LLM API 对消息顺序的要求(如 system 消息必须在最前)。但这仍属业务逻辑问题,非代码层面的明确 Bug。

经过仔细分析,本 hunk 实际上没有空指针、数组越界、死循环、数据竞争或明确的安全漏洞。对 nil slice 的处理正确,索引操作在 len(messages) > 0 时安全。 因此,本次变更未发现明显问题

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已确认无问题,不需要修改

return recall
}

func (r *Ha3ChatRecall) GetCandidateItems(user *module.User, context *pairecctx.RecommendContext) []*module.Item {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在初始化过程中使用 panic 处理错误(如 fs.GetFeatureStoreClientfsClient.GetLLMConfig 失败)可能导致服务崩溃,属于严重稳定性问题。应改为返回 error 或记录日志并降级处理,而非直接 panic。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已确认无问题,不需要修改

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review by ChatGPT

if ok {
fs.ReloadProject()
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

此处存在并发安全问题:在读取 fsInstances[name] 后释放了读锁,然后基于 ok 的判断执行 fs.ReloadProject()。但在读锁释放后、调用 ReloadProject() 前,其他 goroutine 可能已删除或替换了该 name 对应的实例,导致操作了错误或已失效的对象。应将 ReloadProject() 调用包含在读锁保护范围内,或改用其他同步机制确保一致性。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已确认无问题,不需要修改

@lgqfhwy
lgqfhwy marked this pull request as ready for review August 7, 2026 06:10
@lgqfhwy
lgqfhwy merged commit f96da4c into feat/aishopping-field-aware-fallback Aug 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant