docs(comments): 注释中文化 · internal/runtime/memory(31 文件,纯替换)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 只翻注释/文档字符串:820 增 / 820 删 严格 1:1;剥掉整行注释与行尾注释尾巴后,
  逐文件与 HEAD 比对「代码部分差异 = 0」;裁决/design 编号计数逐项与 HEAD 一致
- 保留可执行 SQL 样例、SQLite 报错原文、pgvector/HNSW/ANN/BLOB 等技术词原文
- 复核:go build 绿 + memory/projectindex/vector 三包测试绿;gofmt 仅剩 2 个既有未格式化文件(改动前即如此,未顺手修)
This commit is contained in:
AskaEth committed 2026-10-04 13:32:54 +08:00
1 parent eb1828ee7a
commit 61cf80d5ee
31 files changed
+820 -820

No files matched your search

+54 -54
View File
@@ -10,9 +10,9 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// extractSystemPrompt tells the model.Adapter how to produce candidate memories:
// a JSON array of objects, one per memory worth persisting. Format is locked so
// parseCandidates can unmarshal it deterministically. 溯源(source_conversation)
// extractSystemPrompt 告诉 model.Adapter 如何产出候选记忆:
// 一个 JSON 对象数组,每个对象一条值得持久化的记忆。格式固定,
// 以便 parseCandidates 确定性地反序列化。溯源(source_conversation)
// 刻意不在其中:它由持久化层盖真实 session/message id,绝不由模型生成
// (裁决 04.4.3,见 source.go)。
const extractSystemPrompt = `你是记忆提取器。从对话中提取值得长期记住的记忆项(用户偏好、习惯、约定、重要事件)。
@@ -20,9 +20,9 @@ const extractSystemPrompt = `你是记忆提取器。从对话中提取值得长
{"content":"记忆内容","scope":"user|persona|project","kind":"relationship|world_event","tags":["标签"],"confidence":0.9,"project_id":"可选项目id"}
去噪去重:只保留值得长期记住的,忽略寒暄与临时信息。`
// llmCandidate is the wire shape the extractor asks the LLM to emit: one JSON
// object per candidate memory. Scope/Kind default to the extractor's configured
// values when the model does not provide a valid one. 不含 source_conversation:
// llmCandidate 是提取器要求 LLM 输出的线上形状:每条候选
// 记忆一个 JSON 对象。模型未给出合法值时,Scope/Kind 取提取器配置的
// 默认值。不含 source_conversation:
// 模型即使回填该键也会被忽略(结构化溯源只来自持久化层)。
type llmCandidate struct {
Content string `json:"content"`
@@ -33,14 +33,14 @@ type llmCandidate struct {
ProjectID string `json:"project_id"`
}
// LLMExtractor implements Extractor by asking a model.Adapter to derive candidate
// memory items from a conversation (design §1 LLM 异步自动提取). It runs the LLM
// call synchronously in Extract and on a background goroutine in ExtractAsync.
// LLMExtractor 让 model.Adapter 从对话中推导候选记忆条目
// 来实现 Extractor(design §1 LLM 异步自动提取)。它把 LLM
// 调用在 Extract 中同步执行,在 ExtractAsync 中放到后台 goroutine 执行。
//
// It is idempotent and safe for concurrent use: candidate content is normalized
// and deduplicated against a mutex-guarded seen set, so re-extracting the same
// memory never yields a second copy, and the same content appearing twice inside
// one conversation collapses to a single item.
// 它是幂等且并发安全的:候选内容会规范化,
// 并在互斥锁保护的 seen 集合上去重,因此对同一条
// 记忆重复提取不会产生第二份,同一对话中两次出现的
// 相同内容会合并为一条。
type LLMExtractor struct {
adapter model.Adapter
personaID string
@@ -52,15 +52,15 @@ type LLMExtractor struct {
seen map[string]struct{}
}
// NewLLMExtractor builds an LLMExtractor whose extracted items default to
// ScopePersona + KindRelationship when the model does not specify otherwise, and
// carry personaID/roleID as their author. The persona is a *default*, not the
// authority: a multi-role caller (the runtime loop, design/22 §4.2) builds the
// extractor with a blank persona and stamps each item with the role that produced
// the turn, so pass "" for a role-agnostic extractor. A nil adapter is rejected at
// use time via ErrNilAdapter, matching the lazy construction pattern of the rest
// of the runtime; callers may pass nil to build the struct and wire the adapter
// later.
// NewLLMExtractor 构建 LLMExtractor,当模型未另行指定时,其提取出的条目默认为
// ScopePersona + KindRelationship,并以
// personaID/roleID 为作者。角色是*默认值*,不是
// 权威:多角色调用方(运行时循环,design/22 §4.2)以空
// 角色构建提取器,并在持久化时用产出该轮对话的角色给每条条目
// 盖章,因此要构建与角色无关的提取器就传 ""。nil adapter 在
// 使用期经 ErrNilAdapter 拒绝,与运行时其余部分的
// 惰性构造模式一致;调用方可先传 nil 构建结构体、之后再接上
// adapter。
func NewLLMExtractor(adapter model.Adapter, personaID, roleID string) *LLMExtractor {
return &LLMExtractor{
adapter: adapter,
@@ -72,10 +72,10 @@ func NewLLMExtractor(adapter model.Adapter, personaID, roleID string) *LLMExtrac
}
}
// Extract returns the candidate memory items the adapter derives from
// conversation. A nil adapter returns ErrNilAdapter; a canceled context aborts
// before the LLM call. The response is parsed as a JSON array and then
// deduplicated, so items already extracted by this extractor are dropped.
// Extract 返回 adapter 从 conversation 推导出的候选记忆条目。
// nil adapter 返回 ErrNilAdapter;ctx 被取消会在 LLM 调用前
// 中止。响应按 JSON 数组解析,然后
// 去重,因此本提取器已提取过的条目会被丢弃。
func (e *LLMExtractor) Extract(ctx context.Context, conversation []model.Message) ([]MemoryItem, error) {
if e.adapter == nil {
return nil, ErrNilAdapter
@@ -97,19 +97,19 @@ func (e *LLMExtractor) Extract(ctx context.Context, conversation []model.Message
return e.dedupeAndBuild(cands), nil
}
// AsyncResult is the outcome of an ExtractAsync call: either candidate items or
// the error that aborted extraction.
// AsyncResult 是一次 ExtractAsync 调用的结果:候选条目
// 或中止提取的错误。
type AsyncResult struct {
Items []MemoryItem
Err error
}
// ExtractAsync submits the conversation to a background goroutine and returns a
// channel that delivers exactly one AsyncResult when extraction finishes, closed
// by the goroutine. A nil adapter returns ErrNilAdapter immediately. If ctx is
// canceled before the goroutine runs, the result carries the cancellation error.
// Because Extract is idempotent, repeated async extraction of the same memory is
// safe and never double-records.
// ExtractAsync 把对话提交给后台 goroutine,并返回一个
// channel:提取完成时投递恰好一条 AsyncResult,随后由该 goroutine
// 关闭。nil adapter 立即返回 ErrNilAdapter。若 ctx 在 goroutine 运行前
// 被取消,结果携带取消错误。
// 由于 Extract 幂等,对同一条记忆重复异步提取是
// 安全的,绝不会双写。
func (e *LLMExtractor) ExtractAsync(ctx context.Context, conversation []model.Message) (<-chan AsyncResult, error) {
if e.adapter == nil {
return nil, ErrNilAdapter
@@ -127,8 +127,8 @@ func (e *LLMExtractor) ExtractAsync(ctx context.Context, conversation []model.Me
return out, nil
}
// buildExtractionPrompt turns the conversation into the model request messages:
// the system instruction plus one user message carrying the transcript.
// buildExtractionPrompt 把对话转成模型请求消息:
// 系统指令,外加一条携带对话记录的用户消息。
func buildExtractionPrompt(conversation []model.Message) []model.Message {
var b strings.Builder
for _, m := range conversation {
@@ -143,10 +143,10 @@ func buildExtractionPrompt(conversation []model.Message) []model.Message {
}
}
// parseCandidates decodes the adapter's text response into candidates. A fenced
// ```json block is unwrapped so the model may wrap its array in markdown. Blank
// or non-array content yields no candidates; malformed JSON is an error so the
// caller can decide whether to retry or fall back.
// parseCandidates 把 adapter 的文本响应解码为候选。被围栏包裹的
// ```json 块会被剥离,模型因此可以用 markdown 包住数组。空
// 或非数组内容不产出候选;JSON 格式错误则报错,
// 由调用方决定重试还是回退。
func parseCandidates(s string) ([]llmCandidate, error) {
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "```") {
@@ -172,17 +172,17 @@ func parseCandidates(s string) ([]llmCandidate, error) {
return cands, nil
}
// dedupeAndBuild converts valid candidates to MemoryItems, dropping blank content
// and any content already emitted by this extractor (idempotent dedup). It
// requires e.mu; only valid items mark their dedup key so a malformed candidate
// does not suppress a later valid one with the same text.
// dedupeAndBuild 把合法候选转成 MemoryItem,丢弃空内容
// 以及本提取器已产出的内容(幂等去重)。它
// 要求持有 e.mu;只有合法条目才标记其去重键,因此格式错误的候选
// 不会压掉之后内容相同的合法候选。
//
// The persona is NOT required here even though MemoryItem.validate demands one: a
// role-agnostic extractor (personaID == "") is legitimate — the caller that knows
// the turn's role (the loop) stamps the persona when it persists the item — and
// rejecting the item here would silently drop every memory of a multi-role
// process. Content/scope/kind, which only this package can judge, are still
// enforced. SourceConversation 同理刻意留空:由提取调用点(持久化层)
// 这里不要求 persona,尽管 MemoryItem.validate 要求一个:
// 与角色无关的提取器(personaID == "")是合法的 —— 知道
// 该轮角色的调用方(循环)在持久化条目时盖上角色 —— 而
// 在此拒绝条目会静默丢掉多角色
// 进程的每一条记忆。只有本包能判断的
// content/scope/kind 仍会施加。SourceConversation 同理刻意留空:由提取调用点(持久化层)
// 盖真实 session/message id(裁决 04.4.3,见 source.go)。
func (e *LLMExtractor) dedupeAndBuild(cands []llmCandidate) []MemoryItem {
e.mu.Lock()
@@ -217,13 +217,13 @@ func (e *LLMExtractor) dedupeAndBuild(cands []llmCandidate) []MemoryItem {
return out
}
// dedupeKey normalizes content for idempotent dedup: case-folded and whitespace-
// collapsed, so "记住 A" and "记住 a" collide into one memory.
// dedupeKey 为幂等去重规范化内容:大小写折叠并
// 压缩空白,因此「记住 A」与「记住 a」会归并为同一条记忆。
func dedupeKey(content string) string {
return strings.Join(strings.Fields(strings.ToLower(content)), " ")
}
// normalizeScope returns s when it names a defined scope, otherwise fallback.
// normalizeScope 在 s 命名了已定义作用域时返回 s,否则返回 fallback。
func normalizeScope(s, fallback string) string {
if Scope(s).valid() {
return s
@@ -231,7 +231,7 @@ func normalizeScope(s, fallback string) string {
return fallback
}
// normalizeKind returns s when it names a defined kind, otherwise fallback.
// normalizeKind 在 s 命名了已定义 kind 时返回 s,否则返回 fallback。
func normalizeKind(s, fallback string) string {
if Kind(s).valid() {
return s