Current section

Files

Jump to
cmdc lib cmdc agent system_prompt.ex
Raw

lib/cmdc/agent/system_prompt.ex

defmodule CMDC.Agent.SystemPrompt do
@moduledoc """
系统提示词组装器 — 编译 Agent 最终的 system prompt。
## 组装层级(从底到顶)
1. **BasePrompt** — 核心行为准则 + 可用工具列表
2. **用户 system_prompt** — Blueprint 或直接传入的自定义指令
3. **Blueprint Identity** — name / purpose(若有 Blueprint 配置)
4. **Skills 列表** — 渐进式技能清单(name + description)
5. **Memory 内容** — AGENTS.md 等持久记忆内容
## PromptMode 门控(v0.2 Phase 10B)
通过 `state.prompt_mode` 控制注入哪些段落:
| 模式 | user_prompt | BasePrompt | Identity | Skills | Memory |
|------|-------------|-----------|----------|--------|--------|
| `:full`(默认)|| 完整 `BasePrompt.build/1` ||||
| `:task` || 精简 `BasePrompt.for_mode(:task)` ||||
| `:minimal` || 仅 tools 名称列表 ||||
| `:none` ||||||
`:task` 模式相比 `:full` 典型省 30-50% system prompt token。
## 用法
prompt = CMDC.Agent.SystemPrompt.build(state)
## 与旧项目的差异
- 新增 Skills 注入(对标 DeepAgents SkillsMiddleware)
- 新增 Memory 注入(对标 DeepAgents MemoryMiddleware)
- BasePrompt 独立模块化(旧项目直接硬编码在 SystemPrompt 中)
- Blueprint Identity 信息从 config 中提取
"""
alias CMDC.Agent.{BasePrompt, State}
require Logger
# ==========================================================================
# Public API
# ==========================================================================
@doc """
从 Agent State 编译完整的系统提示词。
`state.prompt_mode` 门控:各模式注入的段落见模块 @moduledoc 表格。
空段自动跳过,段间用双换行分隔。
"""
@spec build(State.t()) :: String.t()
def build(%State{prompt_mode: mode} = state) when mode in [:full, :task, :minimal, :none] do
state
|> sections_for(mode)
|> Enum.reject(&(is_nil(&1) or &1 == ""))
|> Enum.join("\n\n")
end
def build(%State{} = state), do: build(%{state | prompt_mode: :full})
# ==========================================================================
# 模式段落选择
# ==========================================================================
defp sections_for(state, :full) do
[
build_user_prompt(state),
build_base_prompt(state, :full),
build_blueprint_identity(state),
build_skills_section(state),
build_memory_section(state),
build_dynamic_sections(state)
]
end
defp sections_for(state, :task) do
[
build_user_prompt(state),
build_base_prompt(state, :task),
build_blueprint_identity(state),
build_dynamic_sections(state)
]
end
defp sections_for(state, :minimal) do
[
build_user_prompt(state),
build_base_prompt(state, :minimal),
build_blueprint_identity(state),
build_dynamic_sections(state)
]
end
defp sections_for(state, :none) do
[build_user_prompt(state)]
end
# ==========================================================================
# 层 1: 用户自定义 system_prompt
# ==========================================================================
defp build_user_prompt(%State{blueprint_system_prompt: prompt})
when is_binary(prompt) and prompt != "" do
prompt
end
defp build_user_prompt(_state), do: nil
# ==========================================================================
# 层 2: BasePrompt + 工具列表(按 prompt_mode 精简)
# ==========================================================================
defp build_base_prompt(%State{tools: tools}, :full), do: BasePrompt.build(tools)
defp build_base_prompt(%State{tools: tools}, mode), do: BasePrompt.for_mode(tools, mode)
# ==========================================================================
# 层 3: Blueprint Identity(从 config 中提取)
# ==========================================================================
defp build_blueprint_identity(%State{config: config}) when is_map(config) do
blueprint = Map.get(config, :blueprint)
build_identity_from_blueprint(blueprint)
end
defp build_blueprint_identity(_state), do: nil
defp build_identity_from_blueprint(nil), do: nil
defp build_identity_from_blueprint(blueprint) when is_map(blueprint) do
bp = if is_struct(blueprint), do: Map.from_struct(blueprint), else: blueprint
name = bp[:name]
purpose = bp[:purpose] || bp[:description]
case {name, purpose} do
{nil, _} -> nil
{_, nil} -> nil
{n, p} -> "## Agent Identity\n\nYou are **#{n}**: #{p}"
end
end
defp build_identity_from_blueprint(_), do: nil
# ==========================================================================
# 层 4: Skills 列表(支持 Selector 智能筛选)
# ==========================================================================
defp build_skills_section(%State{config: config} = state) when is_map(config) do
skills = Map.get(config, :skills, [])
if is_list(skills) and skills != [] do
selected = maybe_select_skills(skills, config, state)
if selected != [] do
CMDC.Skill.to_prompt_snippet(selected)
else
nil
end
else
nil
end
end
defp build_skills_section(_state), do: nil
defp maybe_select_skills(skills, config, state) do
selector = Map.get(config, :skill_selector)
if is_atom(selector) and not is_nil(selector) do
query = extract_latest_user_query(state)
ctx = build_selector_context(state)
try do
selector.select(skills, query, ctx)
rescue
e ->
Logger.warning(
"[SystemPrompt] Skill Selector #{inspect(selector)} 失败: #{Exception.message(e)},回退全量注入"
)
skills
end
else
skills
end
end
defp extract_latest_user_query(%State{messages: messages}) do
messages
|> Enum.find(fn msg -> msg.role == :user end)
|> case do
%{content: content} when is_binary(content) -> content
_ -> ""
end
end
defp build_selector_context(%State{} = state) do
%CMDC.Context{
session_id: state.session_id,
working_dir: state.working_dir,
model: state.model,
sandbox: state.sandbox,
todos: Map.get(state, :todos, []),
memory_contents: Map.get(state, :memory_contents, %{}),
user_data: Map.get(state, :user_data, %{}),
turn: state.turn_count,
total_tokens: get_in(state.token_usage, [:total_tokens]) || 0,
cost_usd: state.cost_usd
}
end
# ==========================================================================
# 层 5: Memory 内容
# ==========================================================================
defp build_memory_section(%State{memory_contents: contents})
when is_map(contents) and map_size(contents) > 0 do
entries =
contents
|> Enum.sort_by(fn {name, _} -> name end)
|> Enum.map_join("\n\n", fn {name, content} ->
"<agent_memory name=\"#{name}\">\n#{content}\n</agent_memory>"
end)
"## Agent Memory\n\n#{entries}"
end
defp build_memory_section(_state), do: nil
# ==========================================================================
# 层 6: Dynamic System Context(v0.3 Phase 11D — Plugin 动态注入)
# ==========================================================================
defp build_dynamic_sections(%State{dynamic_context_sections: sections})
when is_map(sections) and map_size(sections) > 0 do
sections
|> Enum.sort_by(fn {k, _} -> to_string(k) end)
|> Enum.map_join("\n\n", fn {_k, content} -> content end)
end
defp build_dynamic_sections(_state), do: nil
end