Files
Agent-System-Backend/internal/runtime/llm.go
T
weichen 0d74ec94dd Implement the backend: store/bus/runmgr core
Details (original message):
Implement the backend: store/bus/runmgr core, mock and LLM runners (agent-native client), HITL three-branch routing, subagent runs, builtin and declarative HTTP tools, ingest parsers (md/txt/docx/pdf-text), knowledge fulltext, layered settings, profiles, plugin host, REST+SSE API, single-binary entrypoint; e2e and unit tests.
2026-09-29 17:53:24 +00:00

301 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package runtime
import (
"encoding/json"
"fmt"
"strings"
agentllm "agent-native/runtime/llm"
core "agent-system-core"
"agent-system-backend/internal/ctxasm"
)
// LLMRunner 主对话执行器:复用 agent-native 的 OpenAI 兼容流式客户端(不自研接入层)。
// 多轮循环:流式 → 工具调用(画像策略)→ 回灌结果 → 直至无工具调用或预算耗尽。
// 内置三个编排型工具(不经注册表):delegate_task(子代理)、update_plan、update_todos。
type LLMRunner struct{}
func (l *LLMRunner) Name() string { return "llm" }
const (
toolDelegate = "delegate_task"
toolPlan = "update_plan"
toolTodos = "update_todos"
)
func (l *LLMRunner) Execute(rc RunCtx) error {
eff := rc.Settings()
chat := eff.GetString("models.chat.model", "")
base := eff.GetString("models.chat.baseUrl", "")
key := eff.GetString("models.chat.apiKey", "")
if chat == "" || base == "" {
return fmt.Errorf("llm: 主模型未配置(settings: models.chat.baseUrl / models.chat.model;演示模式可开 runtime.mock)")
}
client := &agentllm.Client{BaseURL: base, APIKey: key}
prof := rc.Profile()
// 知识注入:当前消息包含 @knowledge 引用时检索注入(最小实现:显式引用)。
var knowledge []core.RetrievalHit
if strings.Contains(currentUserText(rc), "@knowledge") {
q := strings.TrimSpace(strings.ReplaceAll(currentUserText(rc), "@knowledge", ""))
if hits, err := rc.Retrieve(core.RetrievalQuery{Text: q}); err == nil {
knowledge = hits
}
}
// 组装初始上下文。
hist := rc.History()
var currentUser core.Message
for i := len(hist) - 1; i >= 0; i-- {
if hist[i].Role == core.RoleUser {
currentUser = hist[i]
break
}
}
msgs, _ := ctxasm.AssembleInput(prof, eff, hist, currentUser, rc.Attachments(), knowledge)
// 声明工具:编排型内置 + 注册表中画像允许的工具。
decls := []agentllm.ToolDecl{
{Name: toolDelegate, Description: "派生一个子代理执行独立子任务(如检索、撰写),返回其结果摘要", Parameters: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string","description":"子代理角色名"},"task":{"type":"string","description":"子任务描述"},"hint":{"type":"string","description":"执行提示"}},"required":["name","task"]}`)},
{Name: toolPlan, Description: "创建或更新当前会话的计划(步骤列表)", Parameters: json.RawMessage(`{"type":"object","properties":{"title":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"},"status":{"type":"string","enum":["pending","in_progress","done","skipped"]}},"required":["id","text","status"]}}},"required":["title","steps"]}`)},
{Name: toolTodos, Description: "更新当前会话的待办清单", Parameters: json.RawMessage(`{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"},"status":{"type":"string","enum":["pending","in_progress","done"]}},"required":["id","text","status"]}}},"required":["items"]}`)},
}
var toolDescs []core.ToolDesc
for _, t := range toolList(rc) {
if prof.Tools.ToolAllowed(t.Name) {
toolDescs = append(toolDescs, t)
decls = append(decls, agentllm.ToolDecl{Name: t.Name, Description: t.Description, Parameters: t.Parameters})
}
}
var finalParts []core.Part
usageTotal := core.Usage{}
maxTurns := 16
if prof.Budget.MaxTurns != nil {
maxTurns = *prof.Budget.MaxTurns
}
maxTools := 40
if prof.Budget.MaxToolCalls != nil {
maxTools = *prof.Budget.MaxToolCalls
}
toolCallsMade := 0
thinkingIdx, textIdx := 0, 0
for turn := 0; turn < maxTurns; turn++ {
select {
case <-rc.Context().Done():
return rc.Context().Err()
default:
}
var turnThinking, turnText strings.Builder
var calls []agentllm.ToolCall
var usage agentllm.Usage
ev := client.Stream(rc.Context(), chat, msgs, decls, agentllm.StreamOptions{})
phaseEmitted := false
for e := range ev {
switch e.Kind {
case agentllm.KindTextDelta:
if !phaseEmitted {
rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseResponding})
phaseEmitted = true
}
turnText.WriteString(e.Text)
rc.Emit(core.EvMessageDelta, core.MessageDeltaPayload{Index: textIdx, Text: e.Text})
case agentllm.KindReasoningDelta:
turnThinking.WriteString(e.ReasoningDelta)
rc.Emit(core.EvThinkingDelta, core.ThinkingDeltaPayload{Index: thinkingIdx, Text: e.ReasoningDelta})
case agentllm.KindToolCallDelta:
// 累积由 message_end 的 ToolCalls 提供;此处忽略增量。
case agentllm.KindMessageEnd:
calls = e.Message.ToolCalls
usage = e.Usage
case agentllm.KindError:
return fmt.Errorf("llm stream: %s (retryable=%v)", e.ErrorMessage, e.Retryable)
}
}
usageTotal.PromptTokens += usage.PromptTokens
usageTotal.CompletionTokens += usage.CompletionTokens
usageTotal.TotalTokens += usage.TotalTokens
// 记录本轮部件。
if turnThinking.Len() > 0 {
finalParts = append(finalParts, core.Part{Type: core.PartThinking, Text: turnThinking.String(), Process: true})
thinkingIdx++
}
if turnText.Len() > 0 {
finalParts = append(finalParts, core.Part{Type: core.PartText, Text: turnText.String()})
textIdx++
}
if len(calls) == 0 {
// 终轮:无工具调用。
if len(finalParts) == 0 {
finalParts = append(finalParts, core.Part{Type: core.PartText, Text: "(模型未返回内容)"})
}
rc.SetAssistantParts(finalParts)
return nil
}
// 工具轮。
rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseActing})
msgs = append(msgs, agentllm.Message{Role: agentllm.RoleAssistant, Content: turnText.String(), ToolCalls: calls, StopReason: agentllm.StopToolCalls})
for _, call := range calls {
toolCallsMade++
if toolCallsMade > maxTools {
return fmt.Errorf("budget: 工具调用超过上限 %d(profile.budget.maxToolCalls)", maxTools)
}
rc.Emit(core.EvToolCall, core.ToolCallPayload{CallID: call.ID, Name: call.Name, Arguments: call.Arguments})
var out string
var err error
var artifactRef *core.ArtifactRef
switch call.Name {
case toolDelegate:
var a struct{ Name, Task, Hint string }
json.Unmarshal([]byte(call.Arguments), &a)
res, serr := rc.SpawnSubagent(orDefault(a.Name, "子代理"), a.Task, a.Hint)
if serr != nil {
out = "子代理失败:" + serr.Error()
} else {
out = orDefault(res.Summary, "子代理完成")
}
case toolPlan:
var p core.PlanSnapshot
if err := json.Unmarshal([]byte(call.Arguments), &p); err == nil {
rc.UpdatePlan(p)
out = "计划已更新"
} else {
out = "计划参数错误: " + err.Error()
}
case toolTodos:
var t core.TodoList
if err := json.Unmarshal([]byte(call.Arguments), &t); err == nil {
rc.UpdateTodos(t)
out = "待办已更新"
} else {
out = "待办参数错误: " + err.Error()
}
default:
res := rc.ExecuteTool(core.ToolCallRequest{CallID: call.ID, Name: call.Name, Arguments: json.RawMessage(call.Arguments)})
if res.Status != "ok" {
err = fmt.Errorf("%s", res.Output)
} else {
out = res.Output
}
if res.Artifact != nil {
artifactRef = &core.ArtifactRef{ArtifactID: res.Artifact.ID, Name: res.Artifact.Name, Kind: res.Artifact.Kind}
}
rc.Emit(core.EvToolResult, core.ToolResultPayload{CallID: call.ID, Status: res.Status, Summary: summarize(out, err), Truncated: res.Truncated})
}
if call.Name == toolDelegate || call.Name == toolPlan || call.Name == toolTodos {
rc.Emit(core.EvToolResult, core.ToolResultPayload{CallID: call.ID, Status: "ok", Summary: summarize(out, err)})
}
summary := ""
if err != nil {
summary = "error: " + err.Error()
out = summary
}
finalParts = append(finalParts,
core.Part{Type: core.PartToolCall, Process: true, ToolCall: &core.ToolCallPart{ID: call.ID, Name: call.Name, Arguments: call.Arguments}},
core.Part{Type: core.PartToolResult, Process: true, ToolResult: &core.ToolResultPart{CallID: call.ID, Status: statusOf(err), Summary: summary}},
)
if artifactRef != nil {
finalParts = append(finalParts, core.Part{Type: core.PartArtifactRef, Artifact: artifactRef})
}
// 回灌工具结果(截断保护)。
if len(out) > 16000 {
out = out[:16000] + "…(超长截断)"
}
msgs = append(msgs, agentllm.Message{Role: agentllm.RoleTool, ToolCallID: call.ID, Name: call.Name, Content: out, IsError: err != nil})
}
}
return fmt.Errorf("budget: 轮次超过上限 %d(profile.budget.maxTurns)", maxTurns)
}
// SubLLMRunner 子代理执行器:轻量模型单轮完成任务并返回摘要。
type SubLLMRunner struct{}
func (l *SubLLMRunner) Name() string { return "llm-subagent" }
func (l *SubLLMRunner) Execute(rc RunCtx) error {
eff := rc.Settings()
model := eff.GetString("models.lightweight.model", "")
base := eff.GetString("models.lightweight.baseUrl", "")
key := eff.GetString("models.lightweight.apiKey", "")
if model == "" || base == "" {
model = eff.GetString("models.chat.model", "")
base = eff.GetString("models.chat.baseUrl", "")
key = eff.GetString("models.chat.apiKey", "")
}
if model == "" || base == "" {
return fmt.Errorf("llm-subagent: 模型未配置")
}
rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: rc.Run().ID, Note: "执行子任务……", Phase: core.PhaseActing})
client := &agentllm.Client{BaseURL: base, APIKey: key}
msgs := []agentllm.Message{
{Role: agentllm.RoleSystem, Content: "你是被派生的子代理。独立完成给定子任务,输出简明结果摘要(不超过 300 字)。"},
{Role: agentllm.RoleUser, Content: rc.Subtask()},
}
var sb strings.Builder
for e := range client.Stream(rc.Context(), model, msgs, nil, agentllm.StreamOptions{}) {
if e.Kind == agentllm.KindTextDelta {
sb.WriteString(e.Text)
rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: rc.Run().ID, Note: truncateRun(sb.String(), 80)})
}
if e.Kind == agentllm.KindError {
return fmt.Errorf("subagent stream: %s", e.ErrorMessage)
}
}
rc.SetAssistantParts([]core.Part{{Type: core.PartText, Text: sb.String()}})
return nil
}
func currentUserText(rc RunCtx) string {
hist := rc.History()
for i := len(hist) - 1; i >= 0; i-- {
if hist[i].Role == core.RoleUser {
for _, p := range hist[i].Parts {
if p.Type == core.PartText {
return p.Text
}
}
}
}
return ""
}
func toolList(rc RunCtx) []core.ToolDesc { return rc.Tools() }
func orDefault(s, def string) string {
if strings.TrimSpace(s) == "" {
return def
}
return s
}
func summarize(out string, err error) string {
if err != nil {
return truncateRun(err.Error(), 160)
}
return truncateRun(out, 160)
}
func statusOf(err error) string {
if err != nil {
return "error"
}
return "ok"
}
func truncateRun(s string, n int) string {
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[:n]) + "…"
}