docs(plugin-arch): 归档插件架构迁移评估 + plan 第11节整改计划

- docs/zh/架构迁移评估.md: C ABI→子进程+共享内存完整迁移论证(1621行)
- docs/zh/experiments/: 18项可复跑可行性实验(架构评估的所有数字来源)
- plan.md §11: 11.1~11.9 插件架构缺陷修复清单(唯一权威编号)
- main 保持干净,本批次为 update 特性分支的整改起点
This commit is contained in:
dev
2026-08-31 11:45:52 +08:00
parent c4219767d4
commit 40a3efa955
34 changed files with 3552 additions and 0 deletions

View File

@ -0,0 +1,90 @@
//go:build ignore
package main
import (
"encoding/json"
"fmt"
"strings"
"sync"
)
// 完全复刻内核 loader.go case 2 + templates.go go_invoke_stage 的链路
type StageCtx struct {
mu sync.RWMutex
LLMText string
ToolRes []string
}
func (c *StageCtx) Lock() { c.mu.Lock() }
func (c *StageCtx) Unlock() { c.mu.Unlock() }
func (c *StageCtx) RLock() { c.mu.RLock() }
func (c *StageCtx) RUnlock() { c.mu.RUnlock() }
// === 模拟外部插件(副本模型)===
func externalPlugin(tag string, ctxJSON string) string {
// go_invoke_stage: 新建全新对象
sc := &StageCtx{}
var m map[string]interface{}
json.Unmarshal([]byte(ctxJSON), &m)
if v, ok := m["llm_text"].(string); ok { sc.LLMText = v }
// 插件 handler:ctx.Lock() 锁的是这个新对象 → 空转
sc.Lock()
sc.LLMText = sc.LLMText + "[" + tag + "]"
sc.Unlock()
out, _ := json.Marshal(map[string]interface{}{"llm_text": sc.LLMText})
return string(out)
}
// === 模拟内核 case 2 handler ===
func kernelStageHandler(sc *StageCtx, tag string) {
sc.RLock()
snap, _ := json.Marshal(map[string]interface{}{"llm_text": sc.LLMText})
sc.RUnlock()
result := externalPlugin(tag, string(snap))
// applyStageResult
var m map[string]interface{}
json.Unmarshal([]byte(result), &m)
sc.Lock()
if v, ok := m["llm_text"].(string); ok { sc.LLMText = v }
sc.Unlock()
}
// === 内置插件:直接改同一对象 ===
func nativePlugin(sc *StageCtx, tag string) {
sc.Lock()
sc.LLMText = sc.LLMText + "[" + tag + "]"
sc.Unlock()
}
func runCase(name string, fn func(*StageCtx, string), tags []string, rounds int) {
lost := 0
for r := 0; r < rounds; r++ {
sc := &StageCtx{LLMText: "BASE"}
var wg sync.WaitGroup
for _, t := range tags {
wg.Add(1)
go func(t string) { defer wg.Done(); fn(sc, t) }(t)
}
wg.Wait()
// 检查是否所有 tag 都在
for _, t := range tags {
if !strings.Contains(sc.LLMText, "["+t+"]") { lost++; break }
}
}
fmt.Printf(" %-28s %d/%d 轮出现修改丢失 (%.1f%%)\n", name, lost, rounds, float64(lost)/float64(rounds)*100)
}
func main() {
tags := []string{"A", "B", "C", "D", "E"}
fmt.Println("5 个插件并发在 StageBeforeToolcall 追加标记,各 2000 轮:")
fmt.Println()
runCase("内置插件(共享同一对象)", nativePlugin, tags, 2000)
runCase("外部插件(快照-副本-写回)", kernelStageHandler, tags, 2000)
fmt.Println()
fmt.Println("→ 副本模型下 read-modify-write 非原子:快照与写回之间的窗口导致覆盖")
}

View File

@ -0,0 +1,101 @@
//go:build ignore
package main
// 精确复刻现网 AfterToolcall 上 sanitizer(Global,改写) + weather(OwnTools,只读) 的并发
import (
"encoding/json"
"fmt"
"strings"
"sync"
)
type ToolResult struct {
Name string `json:"name"`
Plugin string `json:"plugin"`
Result interface{} `json:"result"`
}
type Ctx struct {
mu sync.RWMutex
ToolRes []ToolResult
}
func (c *Ctx) Lock(){c.mu.Lock()}; func (c *Ctx) Unlock(){c.mu.Unlock()}
func (c *Ctx) RLock(){c.mu.RLock()}; func (c *Ctx) RUnlock(){c.mu.RUnlock()}
func cleanText(s string) string {
// 模拟 sanitizer:去掉 ANSI/坏字节
return strings.ReplaceAll(s, "\x1b[31m", "")
}
// 内核 case 2 handler(外部插件通用路径)
func kernelExternal(sc *Ctx, pluginFn func(*Ctx)) {
// 1. 快照
sc.RLock()
snap, _ := json.Marshal(map[string]interface{}{"tool_results": sc.ToolRes})
sc.RUnlock()
// 2. go_invoke_stage: 插件进程内全新对象
local := &Ctx{}
var m map[string]interface{}
json.Unmarshal(snap, &m)
if v, ok := m["tool_results"]; ok {
b, _ := json.Marshal(v)
json.Unmarshal(b, &local.ToolRes)
}
// 3. 插件 handler 跑在副本上
pluginFn(local)
// 4. stageContextWritable: 无条件回传 tool_results
out := map[string]interface{}{}
if len(local.ToolRes) > 0 { out["tool_results"] = local.ToolRes }
rb, _ := json.Marshal(out)
// 5. applyStageResult 写回内核
var rm map[string]interface{}
json.Unmarshal(rb, &rm)
sc.Lock()
if v, ok := rm["tool_results"]; ok {
b, _ := json.Marshal(v)
var trs []ToolResult
if json.Unmarshal(b, &trs) == nil { sc.ToolRes = trs }
}
sc.Unlock()
}
func sanitizerStage(ctx *Ctx) {
ctx.Lock(); defer ctx.Unlock()
for i, tr := range ctx.ToolRes {
if s, ok := tr.Result.(string); ok {
ctx.ToolRes[i].Result = cleanText(s)
}
}
}
func weatherStage(ctx *Ctx) {
ctx.Lock(); defer ctx.Unlock()
// 只读打印,不改(own_tools scope 已匹配)
_ = len(ctx.ToolRes)
}
func main() {
const rounds = 3000
dirty := "\x1b[31m晴 25°C"
polluted := 0
for r := 0; r < rounds; r++ {
sc := &Ctx{ToolRes: []ToolResult{{Name:"weather_query", Plugin:"weather", Result: dirty}}}
var wg sync.WaitGroup
wg.Add(2)
go func(){ defer wg.Done(); kernelExternal(sc, sanitizerStage) }()
go func(){ defer wg.Done(); kernelExternal(sc, weatherStage) }()
wg.Wait()
if s, ok := sc.ToolRes[0].Result.(string); ok && strings.Contains(s, "\x1b[31m") {
polluted++
}
}
fmt.Printf("现网场景复刻:模型调用 weather_query,sanitizer+weather 并发跑 AfterToolcall\n")
fmt.Printf(" %d 轮中 %d 轮清洗结果被覆盖 (%.1f%%)\n", rounds, polluted, float64(polluted)/rounds*100)
if polluted > 0 {
fmt.Printf("\n ⚠️ 确认:weather 回传的未清洗快照覆盖了 sanitizer 的清洗结果\n")
fmt.Printf(" → 脏数据(ANSI 转义)进入 LLM 上下文\n")
}
}