agent-o11y-demo

Download zip
Version
0.1.1
License
MulanPSL-2.0
Last updated
last month
Downloads
14

#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 结构体中,随后传递给 ClientAgent,避免在业务代码中散落环境变量读取逻辑。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(保持 .envOTEL_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 报 unusedasync fn / async test 语法需要此包,但编译器只检测 @async.xxx 显式调用,不把关键字本身算作"使用"。
  2. 测试依赖报 unused@sdk 等):这些包在测试文件中使用,但 MoonBit 的 moon.pkg 是包级配置,编译器不把测试文件中的使用算作"库的使用"。
  3. agent-telemetry 包与根应用通过 moon.work 组成工作区;根应用导入本地 cybershang/agent-telemetry 包。
  4. agent-telemetry 默认后端为 nativeopentelemetry/otlp 依赖的 async/httpasync/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.