agent-o11y-demo

    Download zip
    Version
    0.1.1
    License
    MulanPSL-2.0
    Last updated
    3 months ago
    Downloads
    46

    #Agent Observability Demo

    Check & Test

    参加 MoonBit 2026开源大赛,目标是智能体可观测。 本人项目内容包含:

    智能体插桩库已发布到 MoonBit 官方包仓库 mooncakes.io,在本仓库中作为submodule。

    #演示Demo

    #实现了基础的交互和LLM插桩

    视频:https://www.bilibili.com/video/BV1n4EZ61EmU

    Agent基础交互,包含多轮对话和工具调用: Agent interactive

    使用OTEL_STDOUT开启遥测信号回显: agent otel echo

    使用CAPTURE_CONTENT开启对用户输入和LLM响应的采集: agent content capture

    #Grafana 仪表盘

    使用 GreptimeDB + Grafana 部署的完整可观测性面板,覆盖 Traces、Metrics、Logs 三大支柱:

    Grafana Dashboard

    部署方式见 deploy/greptime/README.md。

    #Agent架构

    flowchart TD User["👤 User / API"] subgraph Agent["🧠 Agent"] direction TB A1["多轮对话状态管理"] A2["上下文窗口累积"] A3["自动 Tool Call 闭环(≤10 轮)"] A4["工具执行记录返回"] end subgraph Client["🔌 Client(LLMGateway)"] C1["通用 GenAI 提供商封装"] C2["HTTP RPC 调用"] C3["请求 / 响应 JSON 序列化"] C4["OpenTelemetry GenAI 插桩"] end API["☁️ GenAI Provider API<br/>(默认 StepFun)"] subgraph TR["🔧 ToolRegistry"] T1["工具注册与路由"] T2["参数解析与执行"] T3["安全策略(命令白名单)"] end subgraph Tools["🛠️ External Tools"] TW["🌤️ get_weather / lookup_city(QWeather)"] TC["💻 execute_command(安全受限)"] end OTel["📊 OpenTelemetry<br/>trace / span / event 插桩"] User --> Agent Agent --> Client Client --> API Agent --> TR TR --> Tools OTel -.->|全链路插桩| Agent OTel -.->|GenAI 语义约定| Client OTel -.-> TR

    #核心模块

    模块文件职责
    Agentagent.mbt对话编排:维护消息历史、自动 tool call 循环、返回结构化结果
    Clientllm.mbt通用 GenAI 客户端:封装 HTTP 调用、管理消息类型、OTel GenAI 插桩
    ToolRegistrytools.mbt工具定义表,供 Agent 注册到 LLM
    Settingssettings.mbt集中管理所有运行时配置:Settings struct + from_env()
    Telemetry Libagent-telemetry/可复用插桩库:provider 初始化、tracer、GenAI/Tool/Agent 语义 helper
    REPL 入口cmd/main/main.mbt配置加载、初始化 OTel、启动交互循环

    #快速开始

    #依赖

    • MoonBit 工具链
    • Linux 系统需安装 build-essential(提供 C 头文件用于 native 编译)

    #配置

    复制示例配置并编辑:

    cp .env.example .env # 编辑 .env,填入你的 API Key

    支持的配置项(环境变量或 .env 文件均可):

    变量说明默认值
    LLM_API_KEYGenAI 提供商 API Key必填
    LLM_PROVIDER提供商标识(用于 OTel)stepfun
    LLM_BASE_URL聊天补全 API 基础 URLhttps://api.stepfun.com/v1
    LLM_MODEL模型名称step-3.7-flash
    LLM_MAX_TOKENS每次请求最大 token 数1024
    AGENT_MAX_TOOL_TURNSAgent 自动 tool call 最大轮数10
    OTEL_STDOUT是否输出 OTel trace 到 stdoutfalse
    CAPTURE_CONTENT是否在 span 中采集用户/助手消息内容false
    QWEATHER_TOKEN和风天气 JWT Token(新 Platform API)必填
    QWEATHER_API_KEY和风天气旧版 Web API Key(作为 QWEATHER_TOKEN 的 fallback)-
    QWEATHER_API_HOST和风天气 API 主机,标准订阅用 https://api.qweather.com,开发版用 https://devapi.qweather.comhttps://devapi.qweather.com

    所有配置在运行时被加载到 Settings 结构体中,随后传递给 Client 与 Agent,避免在业务代码中散落环境变量读取逻辑。QWEATHER_* 配置由 tools.mbt 在工具执行时读取。

    #运行

    # 检查类型 moon check # 运行 REPL moon run cmd/main # 非交互式单次运行(适合 CI / 演示 / 脚本) moon run cmd/main -- --ask "北京今天天气怎么样?"

    #测试

    # 运行所有 async test moon test

    #本地可观测性栈

    项目提供了最小化的本地 Collector + Jaeger 组合:

    cd deploy/minimum docker compose up -d

    启动后:
    • OTLP HTTP receiver: http://localhost:4318
    • OTLP gRPC receiver: http://localhost:4317
    • Jaeger UI: http://localhost:16686

    运行 REPL 并导出 trace 到 Collector(保持 .env 中 OTEL_STDOUT=false 或直接覆盖环境变量):

    OTEL_STDOUT=false moon run cmd/main

    发送一条消息后,打开 http://localhost:16686 即可在 Jaeger 中查看 trace。Service 名称为 agent-observability(可通过 OTEL_SERVICE_NAME 环境变量覆盖)。

    Batch Span Processor 针对交互式 REPL 做了调优,你也可以通过标准环境变量覆盖:

    变量默认值
    OTEL_BSP_MAX_QUEUE_SIZE64
    OTEL_BSP_MAX_EXPORT_BATCH_SIZE16
    OTEL_BSP_SCHEDULE_DELAY1000
    OTEL_BSP_EXPORT_TIMEOUT5000

    #agent-telemetry 库

    仓库中的 agent-telemetry/ 是一个独立的 MoonBit 包,封装了 Agent/LLM/Tool 场景的 OpenTelemetry 插桩。原 agent-observability 应用已改用此库实现。

    安装方式:moon add cybershang/agent-telemetry

    详细 API 文档与使用示例请参见 agent-telemetry/README.md。

    #项目结构

    agent-observability/ ├── agent-telemetry/ # 独立 MoonBit 插桩库(已发布到 mooncakes.io) │ # 封装 OTel 初始化、tracer、GenAI/Tool/Agent 语义 helper ├── cmd/main/ # REPL 可执行入口 ├── deploy/ # 一键部署配置 │ ├── minimum/ # 本地 Collector + Jaeger(docker-compose) │ └── greptime/ # 生产级 GreptimeDB + Grafana 栈 ├── docs/ # 文档 │ ├── instrumentation.md # 插桩位置与 Span 命名详解 │ └── findings.md # 开发过程中的技术发现记录 ├── scripts/ # 辅助脚本 ├── proposal.md # 比赛申报书 ├── report.md # 结项报告 ├── AGENTS.md # 开发指南与约定 ├── .env.example # 环境变量配置模板 └── moon.work # 工作区定义(根包 + agent-telemetry)

    #技术栈

    层级技术
    语言MoonBit
    运行时moonbitlang/async — 原生异步运行时
    构建目标Native
    默认 LLM 提供商StepFun API
    可观测性OpenTelemetry(已实现)

    #已知问题

    #moon check 中的 unused_package 警告

    运行 moon check 时可能会出现若干 unused_package 警告,不影响功能,原因如下:

    1. moonbitlang/async 报 unused:async fn / async test 语法需要此包,但编译器只检测 @async.xxx 显式调用,不把关键字本身算作"使用"。
    2. 测试依赖报 unused(@sdk 等):这些包在测试文件中使用,但 MoonBit 的 moon.pkg 是包级配置,编译器不把测试文件中的使用算作"库的使用"。
    3. agent-telemetry 包与根应用通过 moon.work 组成工作区;根应用导入本地 cybershang/agent-telemetry 包。
    4. agent-telemetry 默认后端为 native:opentelemetry/otlp 依赖的 async/http、async/socket 接口只在 native 后端可用,因此库模块声明了 preferred_target = "native"。

    MoonBit 目前不支持文件级导入或独立的测试子包,因此这些警告在当前结构下无法消除。CI 已移除 --deny-warn 以避免因此失败。

    #许可证

    本项目采用 木兰宽松许可证,第 2 版(Mulan PSL v2)开源许可。 Copyright (c) 2026 Yingjie Shang

    ToolHandler

    type ToolHandler = async (String) -> String

    A tool handler takes a JSON arguments string and returns a result string.

    Agent

    pub struct Agent {
    client : Client
    messages : Array[Message]
    tools : Array[Tool]
    max_tool_turns : Int
    capture_content : Bool
    }

    An agent that orchestrates LLM chat with tool calls.

    The agent maintains conversation history and automatically executes any tool calls requested by the LLM, feeding results back until a final response is produced.

    Agent::new

    fn Agent::new(client : Client, tools? : Array[Tool], max_tool_turns? : Int, capture_content? : Bool) -> Agent

    Create a new agent with the given client and optional tools.

    Agent::run

    async fn Agent::run(self : Agent, prompt : String) -> AgentTurnResult

    Run one turn of conversation with the agent.

    The prompt is appended to the conversation history, then the agent loops over LLM responses and tool calls until a final text response is received. Returns the assistant's reply and a record of any tool calls executed during the turn.

    AgentTurnResult

    pub struct AgentTurnResult {
    reply : String
    tool_calls : Array[ToolCallRecord]
    } derive(
    Debug
    )

    Result of a single agent turn, including the assistant's reply and any tool calls that were executed.

    Client

    pub struct Client {
    provider_name : String
    base_url : String
    model : String
    api_key : String
    max_tokens : Int
    capture_content : Bool
    }

    A client for a GenAI provider.

    Client::chat

    async fn Client::chat(self : Client, messages : Array[Message], tools? : Array[Tool], parent_context? :
    Context
    ) -> LLMResponse

    Send a chat request to the configured GenAI provider.

    This method is instrumented with OpenTelemetry GenAI semantic conventions:
    • Span name: gen_ai.chat
    • gen_ai.operation.name = "chat"
    • gen_ai.provider.name = provider_name
    • gen_ai.request.model = model
    • gen_ai.request.max_tokens = max_tokens
    • gen_ai.usage.input_tokens (from response)
    • gen_ai.usage.output_tokens (from response)
    • gen_ai.response.id (from response)
    • gen_ai.response.model (from response)
    • gen_ai.response.finish_reasons (from response)
    • gen_ai.input.messages and gen_ai.output.messages when capture_content is enabled
    • gen_ai.client.token.usage metric for input/output tokens
    • Conversation logs when capture_content is enabled

    Client::from_settings

    fn Client::from_settings(settings : Settings) -> Client

    Create a client from a loaded Settings value.

    Client::new

    fn Client::new(provider_name~ : String, base_url~ : String, model~ : String, api_key~ : String, max_tokens? : Int, capture_content? : Bool) -> Client

    Create a new GenAI client.

    LLMResponse

    pub(all) struct LLMResponse {
    content : String?
    tool_calls : Array[ToolCall]
    finish_reason : String
    response_id : String?
    response_model : String?
    } derive(
    Debug
    )

    Result of a single LLM request.

    Message

    pub struct Message {
    role : String
    content : String?
    tool_calls : Array[ToolCall]?
    tool_call_id : String?
    } derive(
    Debug
    )

    A single message in the conversation history.

    Message::new

    fn Message::new(role? : String, content? : String?, tool_calls? : Array[ToolCall]?, tool_call_id? : String?) -> Message

    Create a new message.

    Message::to_json

    fn Message::to_json(self : Message) -> Json

    Convert a Message to JSON for the API request.

    RegisteredTool

    pub struct RegisteredTool {
    tool : Tool
    handler : async (String) -> String
    }

    A tool definition bundled with its runtime handler.

    Settings

    pub struct Settings {
    provider_name : String
    base_url : String
    model : String
    api_key : String
    max_tokens : Int
    max_tool_turns : Int
    otel_stdout : Bool
    otel_endpoint : String
    capture_content : Bool
    }

    Application settings loaded from environment variables or .env file.

    Settings::from_env

    async fn Settings::from_env() -> Settings

    Load settings from environment variables or .env file.

    Tool

    pub struct Tool {
    name : String
    description : String
    parameters : Json
    } derive(
    Debug
    )

    A tool definition that can be registered with the LLM.

    Tool::new

    fn Tool::new(name~ : String, description~ : String, parameters~ : Json) -> Tool

    Create a new tool definition.

    ToolCall

    pub(all) struct ToolCall {
    id : String
    name : String
    arguments : String
    } derive(
    Debug
    )

    A tool call requested by the LLM.

    ToolCall::to_json

    fn ToolCall::to_json(self : ToolCall) -> Json

    Convert a ToolCall to JSON.

    ToolCallRecord

    pub struct ToolCallRecord {
    call : ToolCall
    result : String
    } derive(
    Debug
    )

    Record of a single tool call executed during an agent turn.

    env

    async fn env(key : String, default? : String) -> String

    Read a configuration value from the process environment or .env file.

    Resolution order: process environment > .env file > default.

    execute_tool

    async fn execute_tool(name : String, arguments : String, parent_context? :
    Context
    ) -> String

    Execute a tool by name with JSON arguments.

    parse_int

    fn parse_int(s : String, default : Int) -> Int

    Parse an integer from a string, returning the default on failure.

    read_dotenv

    async fn read_dotenv(key : String) -> String?

    Read a .env file and look up a key.

    tools

    let tools : Array[Tool]

    Tool definitions exposed to the LLM.

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io